@@@@@@
This commit is contained in:
40
library/components/VabAlert/index.vue
Normal file
40
library/components/VabAlert/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<el-alert
|
||||
:center="center"
|
||||
:closable="closable"
|
||||
:close-text="closeText"
|
||||
:description="description"
|
||||
:effect="effect"
|
||||
:show-icon="showIcon"
|
||||
:title="title"
|
||||
:type="type"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="title || $slots.title" #title>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</template>
|
||||
<template v-if="$slots.default || description" #default>
|
||||
<slot name="default">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElAlert } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabAlert',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
...ElAlert.props,
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
18
library/components/VabApp/index.vue
Normal file
18
library/components/VabApp/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<el-config-provider :button="{ autoInsertSpace: true }" :locale="locale">
|
||||
<router-view />
|
||||
<vab-update v-if="pwa" />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { pwa } from '/@/config'
|
||||
import { enLocale, zhLocale } from '/@/i18n'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabApp',
|
||||
})
|
||||
|
||||
const { locale: language } = useI18n()
|
||||
const locale = computed(() => (language.value === 'en' ? enLocale : zhLocale))
|
||||
</script>
|
||||
30
library/components/VabAppMain/index.vue
Normal file
30
library/components/VabAppMain/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="vab-app-main">
|
||||
<section>
|
||||
<vab-router-view />
|
||||
<vab-footer />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { handleActivePath } from '/@/utils/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabAppMain',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const routesStore = useRoutesStore()
|
||||
const { tab, activeMenu } = storeToRefs(routesStore)
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
if (tab.value.data !== route.matched[0].name) tab.value.data = route.matched[0].name
|
||||
activeMenu.value.data = handleActivePath(route)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
200
library/components/VabAvatar/index.vue
Normal file
200
library/components/VabAvatar/index.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<el-popover
|
||||
v-model:visible="visible"
|
||||
class="vab-avatar"
|
||||
popper-class="vab-avatar-popper"
|
||||
width="188"
|
||||
@hide="handleShow"
|
||||
@show="handleHide"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="avatar-dropdown">
|
||||
<!-- <el-avatar class="user-avatar" :src="avatar" /> -->
|
||||
<img src="../../../public/logo.png" alt="" style="width: 30px; height: 30px;margin-left: 20px; background: #59809b; border-radius: 50%;">
|
||||
<div class="username">
|
||||
<span class="hidden-xs-only">{{ username }}</span>
|
||||
<vab-icon class="vab-dropdown" :class="{ 'vab-dropdown-active': active }" icon="arrow-down-s-line" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="avatar-dropdown" @click="handleCommand('personalCenter')">
|
||||
<!-- <el-avatar class="user-avatar" :src="avatar" /> -->
|
||||
<img src="../../../public/logo.png"
|
||||
style="width: 30px; height: 30px; margin-left: 20px; background: #59809b; border-radius: 50%;">
|
||||
<div class="username">
|
||||
<div>{{ username }}</div>
|
||||
<div class="personal-center">
|
||||
<el-text size="small" type="info">个人中心</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<ul class="el-dropdown-menu">
|
||||
<!-- <li class="el-dropdown-menu__item" @click="handleCommand('changeLog')">
|
||||
<vab-icon icon="file-word-line" />
|
||||
<span>{{ translate('更新日志') }}</span>
|
||||
<el-tag effect="dark" size="small" type="danger">99+</el-tag>
|
||||
</li> -->
|
||||
<li class="el-dropdown-menu__item" @click="handleCommand('logout')">
|
||||
<vab-icon icon="logout-circle-r-line" />
|
||||
<span>{{ translate('退出登录') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { translate } from '/@/i18n'
|
||||
import { useUserStore } from '/@/store/modules/user'
|
||||
import { toLoginRoute } from '/@/utils/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabAvatar',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { avatar, username } = storeToRefs(userStore)
|
||||
const { logout } = userStore
|
||||
const active = ref(false)
|
||||
const visible = ref(false)
|
||||
const handleShow = () => {
|
||||
active.value = false
|
||||
}
|
||||
|
||||
const handleHide = () => {
|
||||
active.value = true
|
||||
}
|
||||
|
||||
const handleCommand = async (command) => {
|
||||
switch (command) {
|
||||
case 'logout': {
|
||||
await logout()
|
||||
await router.push(toLoginRoute(route.fullPath))
|
||||
visible.value = false
|
||||
break
|
||||
}
|
||||
case 'personalCenter': {
|
||||
await router.push('/setting/personalCenter')
|
||||
visible.value = false
|
||||
break
|
||||
}
|
||||
case 'changeLog': {
|
||||
await router.push('/changeLog')
|
||||
visible.value = false
|
||||
break
|
||||
}
|
||||
case 'portal': {
|
||||
await window.open('#/portal')
|
||||
visible.value = false
|
||||
break
|
||||
}
|
||||
case 'dataScreen': {
|
||||
await window.open('#/dataScreen')
|
||||
visible.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
content: '';
|
||||
background: var(--el-color-success);
|
||||
border: 3px solid var(--el-color-white);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
height: 40px;
|
||||
margin-left: 6px;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
[class*='ri-'] {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.vab-avatar-popper {
|
||||
padding: 0 !important;
|
||||
|
||||
.avatar-dropdown {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: start !important;
|
||||
padding: calc(var(--el-padding) / 1.5);
|
||||
|
||||
.user-avatar {
|
||||
margin-left: calc(var(--el-margin) / 2) calc(var(--el-margin) / 2) calc(var(--el-margin) / 2) 0 !important;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
line-height: 20px;
|
||||
|
||||
.personal-center {
|
||||
width: 100%;
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--el-color-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-menu {
|
||||
position: relative;
|
||||
padding: calc(var(--el-padding) / 2);
|
||||
|
||||
&__item {
|
||||
.el-tag {
|
||||
position: absolute;
|
||||
right: 17.5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
.el-divider--horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
library/components/VabBreadcrumb/index.vue
Normal file
49
library/components/VabBreadcrumb/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-breadcrumb class="vab-breadcrumb" separator="/">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index" :to="handleTo(item.redirect)">
|
||||
<vab-icon v-if="item.meta && item.meta.icon" :icon="item.meta.icon" />
|
||||
<span>{{ translate(item.meta.title) }}</span>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { handleMatched } from '/@/utils/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabBreadcrumb',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const routesStore = useRoutesStore()
|
||||
const { getBreadcrumbRoutes: breadcrumbRoutes } = storeToRefs(routesStore)
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
const matchedRoutes = handleMatched(breadcrumbRoutes.value, route.fullPath).filter((item) => !item.meta.breadcrumbHidden)
|
||||
if (matchedRoutes.length > 0) return matchedRoutes
|
||||
else return handleMatched(breadcrumbRoutes.value, route.path).filter((item) => !item.meta.breadcrumbHidden)
|
||||
})
|
||||
|
||||
const handleTo = (path) => {
|
||||
if (path) return { path }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--el-nav-height);
|
||||
|
||||
:deep() {
|
||||
.el-breadcrumb__item {
|
||||
.el-breadcrumb__inner {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
library/components/VabCard/index.vue
Normal file
76
library/components/VabCard/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<el-card :body-class="bodyClass" :body-style="bodyStyle" :shadow="shadow" v-bind="$attrs">
|
||||
<template v-if="$slots.header || title" #header>
|
||||
<slot v-if="$slots.header" name="header"></slot>
|
||||
<template v-else>{{ title }}</template>
|
||||
</template>
|
||||
<el-skeleton v-if="skeleton" animated :loading="skeletonShow" :rows="skeletonRows">
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<slot v-else></slot>
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElCard } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabCard',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
...ElCard.props,
|
||||
shadow: {
|
||||
type: String,
|
||||
default: 'never',
|
||||
},
|
||||
skeleton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
skeletonRows: {
|
||||
type: Number,
|
||||
default: 5, //显示的数量会比传入的数量多 1
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const skeletonShow = ref(true)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
skeletonShow.value = false
|
||||
}, 500)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.el-card {
|
||||
.el-card__header {
|
||||
font-weight: 500;
|
||||
|
||||
[class*='ri-'] {
|
||||
background-image: linear-gradient(120deg, #bd34fe 30%, var(--el-color-primary));
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.el-skeleton {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
library/components/VabColorPicker/index.vue
Normal file
64
library/components/VabColorPicker/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="'technology' != theme.themeName" class="vab-color-picker" style="margin-left: var(--el-margin)">
|
||||
<el-color-picker
|
||||
v-model="theme.color"
|
||||
popper-class="vab-color-picker-popper"
|
||||
:predefine="predefineColors"
|
||||
@active-change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { color as _color } from '/@/config/'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabColorPicker',
|
||||
})
|
||||
|
||||
const predefineColors = ref([
|
||||
_color,
|
||||
'#1e90ff',
|
||||
'#4e6ef2',
|
||||
'#0052d9',
|
||||
'#3fb884',
|
||||
'#16baa9',
|
||||
'#07c160',
|
||||
'#009688',
|
||||
'#6954f0',
|
||||
'#7b40f2',
|
||||
'#f01414',
|
||||
])
|
||||
const settingsStore = useSettingsStore()
|
||||
const { updateTheme, saveTheme } = settingsStore
|
||||
const { theme } = storeToRefs(settingsStore)
|
||||
|
||||
const handleChange = (value) => {
|
||||
theme.value.color = value
|
||||
updateTheme()
|
||||
saveTheme()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 还原默认
|
||||
$sub('shop-vite-reset-color', () => {
|
||||
handleChange(_color)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-color-picker-popper {
|
||||
box-sizing: content-box !important;
|
||||
padding: calc(var(--el-padding) / 2);
|
||||
|
||||
.el-color-dropdown__link-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-color-dropdown__btns {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
library/components/VabColorfulCard/index.vue
Normal file
77
library/components/VabColorfulCard/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<el-card
|
||||
:body-style="bodyStyle"
|
||||
class="vab-colorful-card"
|
||||
:shadow="shadow"
|
||||
:style="
|
||||
style
|
||||
? style
|
||||
: {
|
||||
background: `linear-gradient(120deg, ${colorFrom} 10%, ${colorTo})`,
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<vab-icon v-if="icon" :icon="icon" />
|
||||
<slot></slot>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElCard } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabColorfulCard',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
...ElCard.props,
|
||||
shadow: {
|
||||
type: String,
|
||||
default: 'never',
|
||||
},
|
||||
colorFrom: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorTo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-colorful-card {
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
cursor: pointer;
|
||||
|
||||
:deep() {
|
||||
.el-card__header {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
font-size: 60px;
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
440
library/components/VabColumnBar/index.vue
Normal file
440
library/components/VabColumnBar/index.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<el-scrollbar
|
||||
class="vab-column-bar"
|
||||
:class="{
|
||||
'is-collapse': collapse,
|
||||
['vab-column-bar-' + theme.columnStyle]: true,
|
||||
}"
|
||||
>
|
||||
<vab-logo style="z-index: 999" />
|
||||
<el-tabs v-model="tab.data" tab-position="left" @tab-click="handleTabClick">
|
||||
<template v-for="(item, index) in routes" :key="index + item.name">
|
||||
<el-tab-pane :name="item.name">
|
||||
<template #label>
|
||||
<div
|
||||
class="vab-column-grid"
|
||||
:class="{
|
||||
['vab-column-grid-' + theme.columnStyle]: true,
|
||||
}"
|
||||
:title="translate(item.meta.title)"
|
||||
>
|
||||
<div>
|
||||
<vab-icon v-if="item.meta.icon" :icon="item.meta.icon" :is-custom-svg="item.meta.isCustomSvg" />
|
||||
<span v-if="translate(item.meta.title).length < 4">
|
||||
{{ translate(item.meta.title) }}
|
||||
</span>
|
||||
<span v-else style="font-size: var(--el-font-size-extra-small); zoom: 0.88">
|
||||
{{ translate(item.meta.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
</el-tabs>
|
||||
|
||||
<el-menu
|
||||
ref="menuRef"
|
||||
background-color="var(--el-menu-background-color-second)"
|
||||
:default-active="activeMenu.data"
|
||||
:default-openeds="defaultOpeneds"
|
||||
mode="vertical"
|
||||
:unique-opened="uniqueOpened"
|
||||
>
|
||||
<vab-menu v-for="item in partialRoutes" :key="item.path" :item="item" />
|
||||
</el-menu>
|
||||
<div class="float-fold">
|
||||
<vab-fold fold="contract-left-line" unfold="contract-right-line" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defaultOpeneds, isHashRouterMode, openFirstMenu, uniqueOpened } from '/@/config'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabColumnBar',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { collapse, device, theme } = storeToRefs(settingsStore)
|
||||
const { foldSideBar, openSideBar } = settingsStore
|
||||
const routesStore = useRoutesStore()
|
||||
const {
|
||||
getTab: tab,
|
||||
getTabMenu: tabMenu,
|
||||
getActiveMenu: activeMenu,
|
||||
getRoutes: routes,
|
||||
getPartialRoutes: partialRoutes,
|
||||
} = storeToRefs(routesStore)
|
||||
const menuRef = ref(null)
|
||||
let timer
|
||||
|
||||
const setDefaultOpeneds = () => {
|
||||
timer = setTimeout(() => {
|
||||
defaultOpeneds.forEach((item) => {
|
||||
try {
|
||||
menuRef.value.open(item)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleTabClick = () => {
|
||||
nextTick(() => {
|
||||
if (tabMenu.value.meta.target === '_blank') {
|
||||
if (route.path !== tabMenu.value.path) {
|
||||
isHashRouterMode ? window.open(`#${tabMenu.value.path}`) : window.open(tabMenu.value.path)
|
||||
router.push('/redirect')
|
||||
}
|
||||
} else if (isExternal(tabMenu.value.path)) {
|
||||
window.open(tabMenu.value.path)
|
||||
router.push('/redirect')
|
||||
} else if (openFirstMenu) router.push(tabMenu.value.redirect || tabMenu.value)
|
||||
setDefaultOpeneds()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setDefaultOpeneds()
|
||||
if (theme.value.layout === 'column')
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
const foldUnfold = document.querySelector('.left-panel .fold-unfold')
|
||||
const floatFold = document.querySelector('.float-fold')
|
||||
if (route.meta.noColumn && theme.value.layout === 'column') {
|
||||
if (device.value !== 'mobile') foldSideBar()
|
||||
if (foldUnfold) foldUnfold.style = 'opacity:0'
|
||||
if (floatFold) floatFold.style = 'opacity:0'
|
||||
} else {
|
||||
if (device.value !== 'mobile') openSideBar()
|
||||
if (foldUnfold) foldUnfold.style = 'opacity:1'
|
||||
if (floatFold) floatFold.style = 'opacity:1'
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin active {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
|
||||
i,
|
||||
svg {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.vab-column-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--el-left-menu-width);
|
||||
overflow: hidden;
|
||||
background: var(--el-color-white);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
|
||||
&-vertical,
|
||||
&-card,
|
||||
&-arrow {
|
||||
:deep() {
|
||||
.el-tabs + .el-menu {
|
||||
left: var(--el-left-menu-width-min);
|
||||
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min));
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-horizontal,
|
||||
&-semicircle {
|
||||
.float-fold {
|
||||
left: 26.5px;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.vab-logo-column {
|
||||
.logo {
|
||||
width: calc(var(--el-left-menu-width-min) * 1.4) !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
left: calc(var(--el-left-menu-width-min) * 1.4) !important;
|
||||
width: calc(var(--el-left-menu-width) - calc(var(--el-left-menu-width-min) * 1.4) - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs + .el-menu {
|
||||
left: calc(var(--el-left-menu-width-min) * 1.4);
|
||||
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min) * 1.4);
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-card {
|
||||
:deep() {
|
||||
.el-tabs {
|
||||
.el-tabs__item {
|
||||
padding: 5px !important;
|
||||
|
||||
.vab-column-grid {
|
||||
width: calc(var(--el-left-menu-width-min) - 12px) !important;
|
||||
height: calc(var(--el-left-menu-width-min) - 10px) !important;
|
||||
margin-left: 2px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: transparent !important;
|
||||
|
||||
.vab-column-grid {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs + .el-menu {
|
||||
left: calc(var(--el-left-menu-width-min) + 10px);
|
||||
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min) - 20px);
|
||||
}
|
||||
|
||||
.el-sub-menu .el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
min-width: 180px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
:deep() {
|
||||
.el-tabs {
|
||||
.el-tabs__item {
|
||||
&.is-active {
|
||||
background: transparent !important;
|
||||
|
||||
.vab-column-grid {
|
||||
background: transparent !important;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
content: '';
|
||||
border-color: transparent var(--el-color-white) transparent transparent;
|
||||
border-style: solid dashed dashed;
|
||||
border-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs + .el-menu {
|
||||
left: calc(var(--el-left-menu-width-min) + 10px);
|
||||
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min) - 20px);
|
||||
}
|
||||
|
||||
.el-sub-menu .el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
min-width: 180px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-semicircle {
|
||||
:deep() {
|
||||
.el-tabs {
|
||||
.el-tabs__item {
|
||||
&.is-active {
|
||||
border-top-left-radius: 99px;
|
||||
border-bottom-left-radius: 99px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vab-column-grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: var(--el-left-menu-width-min);
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
|
||||
&-vertical,
|
||||
&-card,
|
||||
&-arrow {
|
||||
justify-content: center;
|
||||
height: var(--el-left-menu-width-min);
|
||||
|
||||
> div {
|
||||
svg,
|
||||
[class*='ri-'] {
|
||||
display: block;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-horizontal,
|
||||
&-semicircle {
|
||||
justify-content: left;
|
||||
width: calc(var(--el-left-menu-width-min) * 1.4);
|
||||
height: calc(var(--el-left-menu-width-min) / 1.4);
|
||||
padding-left: var(--el-padding);
|
||||
|
||||
svg,
|
||||
[class*='ri-'] {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep() {
|
||||
* {
|
||||
transition: var(--el-transition);
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
height: calc(var(--vh, 1vh) * 100 - var(--el-logo-height));
|
||||
|
||||
.el-tabs__header.is-left {
|
||||
margin-right: 0 !important;
|
||||
|
||||
.el-tabs__nav-wrap.is-left {
|
||||
margin-right: 0 !important;
|
||||
background: var(--el-menu-background-color);
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
height: calc(var(--vh, 1vh) * 100 - var(--el-logo-height) * 2);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav {
|
||||
height: calc(var(--vh, 1vh) * 100 - var(--el-logo-height) * 2);
|
||||
background: var(--el-menu-background-color);
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
color: var(--el-color-white);
|
||||
|
||||
&.is-active {
|
||||
margin-right: -1px;
|
||||
background: var(--el-color-primary);
|
||||
|
||||
> .vab-column-grid {
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar.is-left,
|
||||
.el-tabs--left .el-tabs__nav-wrap.is-left::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
margin-top: 10px;
|
||||
border: 0;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: var(--el-menu-item-height);
|
||||
overflow: hidden;
|
||||
line-height: var(--el-menu-item-height);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@include active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapse {
|
||||
:deep() {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.float-fold {
|
||||
position: fixed;
|
||||
bottom: 13px;
|
||||
left: 14px;
|
||||
z-index: 9999;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
:deep() {
|
||||
.fold-unfold,
|
||||
.ri-building-line {
|
||||
font-size: var(--el-font-size-extra-large);
|
||||
color: var(--el-color-white);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
120
library/components/VabDark/index.vue
Normal file
120
library/components/VabDark/index.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<el-switch
|
||||
v-if="'technology' != theme.themeName && 'plain' != theme.themeName && route.path !== '/goods/posterDesign'"
|
||||
v-model="mode"
|
||||
:active-icon="Moon"
|
||||
active-value="dark"
|
||||
class="vab-dark"
|
||||
:inactive-icon="Sunny"
|
||||
inactive-value="light"
|
||||
inline-prompt
|
||||
@click="_toggleDark($event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabDark',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme, mode } = storeToRefs(settingsStore)
|
||||
const { updateMode } = settingsStore
|
||||
|
||||
const _toggleDark = async (event) => {
|
||||
if (typeof document.startViewTransition === 'function') {
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
|
||||
let isDark
|
||||
const transition = document.startViewTransition(() => {
|
||||
const root = document.documentElement
|
||||
isDark = root.classList.contains('dark')
|
||||
root.classList.remove(isDark ? 'dark' : 'light')
|
||||
root.classList.add(isDark ? 'light' : 'dark')
|
||||
handleSetScheme(isDark ? 'light' : 'dark')
|
||||
})
|
||||
await transition.ready.then(() => {
|
||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: isDark ? [...clipPath].reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const toggleDark = useToggle(handleUseDark())
|
||||
await toggleDark()
|
||||
}
|
||||
await updateMode(localStorage.getItem('vueuse-color-scheme'))
|
||||
}
|
||||
|
||||
const handleUseDark = () => {
|
||||
return useDark()
|
||||
}
|
||||
|
||||
const handleGetScheme = () => {
|
||||
return localStorage.getItem('vueuse-color-scheme')
|
||||
}
|
||||
|
||||
const handleSetScheme = (value) => {
|
||||
return localStorage.setItem('vueuse-color-scheme', value)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 还原默认
|
||||
$sub('shop-vite-reset-dark', () => {
|
||||
mode.value = handleGetScheme()
|
||||
if (handleGetScheme() === 'dark') {
|
||||
handleSetScheme('light')
|
||||
handleUseDark()
|
||||
mode.value = 'light'
|
||||
}
|
||||
})
|
||||
|
||||
handleUseDark()
|
||||
if (handleGetScheme() === 'auto') handleSetScheme('light')
|
||||
mode.value = handleGetScheme()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark {
|
||||
&::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
||||
.vab-dark {
|
||||
margin-left: var(--el-margin);
|
||||
}
|
||||
</style>
|
||||
120
library/components/VabDialog/index.vue
Normal file
120
library/components/VabDialog/index.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:align-center="alignCenter"
|
||||
:append-to="appendTo"
|
||||
:append-to-body="appendToBody"
|
||||
:before-close="beforeClose"
|
||||
:center="center"
|
||||
:class="'vab-dialog-' + theme"
|
||||
:close-on-click-modal="closeOnClickModal"
|
||||
:close-on-press-escape="closeOnPressEscape"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:draggable="draggable"
|
||||
:fullscreen="isFullscreen"
|
||||
:lock-scroll="lockScroll"
|
||||
:modal="modal"
|
||||
:modal-class="modalClass"
|
||||
:open-delay="openDelay"
|
||||
:overflow="overflow"
|
||||
:show-close="showClose"
|
||||
:style="{
|
||||
transition: animated ? 'all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),transform 0s' : '',
|
||||
}"
|
||||
:top="top"
|
||||
:width="width"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header">
|
||||
<div class="el-dialog__title" @dblclick="setFullscreen">{{ title }}</div>
|
||||
</slot>
|
||||
<button v-if="showClose" class="el-dialog__headerbtn" type="button" @click="closeDialog">
|
||||
<el-icon class="el-dialog__close">
|
||||
<close />
|
||||
</el-icon>
|
||||
</button>
|
||||
<button v-if="showFullscreen" class="el-dialog__headerbtn" style="right: 51px" type="button" @click="setFullscreen">
|
||||
<vab-icon class="el-dialog__close el-dialog__fullscreen" :icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'" />
|
||||
</button>
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { ElDialog } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabDialog',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
...ElDialog.props,
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showFullscreen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
closeOnClickModal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeOnPressEscape: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'default', //支持default、plain、primary三种
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = useVModel(props, 'modelValue', emit)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false
|
||||
isFullscreen.value = false
|
||||
}
|
||||
|
||||
const setFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
isFullscreen.value = props.fullscreen
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
98
library/components/VabDivider/index.vue
Normal file
98
library/components/VabDivider/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<blockquote
|
||||
v-if="blockquote"
|
||||
class="vab-blockquote"
|
||||
:class="isBorder ? 'vab-blockquote-' + type + ' is-border' : 'vab-blockquote-' + type"
|
||||
>
|
||||
<slot></slot>
|
||||
</blockquote>
|
||||
<fieldset v-else-if="fieldset" class="vab-fieldset">
|
||||
<legend>{{ title }}</legend>
|
||||
<slot></slot>
|
||||
</fieldset>
|
||||
<el-divider v-else :border-style="borderStyle" :content-position="contentPosition" :direction="direction">
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</el-divider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElDivider } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabDivider',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
...ElDivider.props,
|
||||
blockquote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fieldset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary', // 类型: primary / success / warning / danger / info
|
||||
},
|
||||
isBorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-blockquote {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1.8;
|
||||
background-color: var(--el-background-color);
|
||||
border-left: 5px solid var(--el-color-primary);
|
||||
border-radius: 0 var(--el-border-radius-base) var(--el-border-radius-base) 0;
|
||||
|
||||
&-primary {
|
||||
border-left: 5px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
&-success {
|
||||
border-left: 5px solid var(--el-color-success);
|
||||
}
|
||||
|
||||
&-warning {
|
||||
border-left: 5px solid var(--el-color-warning);
|
||||
}
|
||||
|
||||
&-danger {
|
||||
border-left: 5px solid var(--el-color-danger);
|
||||
}
|
||||
|
||||
&-info {
|
||||
border-left: 5px solid var(--el-color-info);
|
||||
}
|
||||
|
||||
&.is-border {
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.vab-fieldset {
|
||||
padding: var(--el-padding);
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
|
||||
legend {
|
||||
padding: 0 var(--el-padding) 0 var(--el-padding);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
library/components/VabDot/index.vue
Normal file
75
library/components/VabDot/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<span :class="'vab-dot vab-dot-' + type"></span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'VabDot',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
values: ['primary', 'success', 'info', 'warning', 'danger'],
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/**
|
||||
* @name: vab-dot
|
||||
* @description: vab圆点动画
|
||||
* @author: sundan
|
||||
* @date: 2024-08-05 22:53:00
|
||||
*/
|
||||
.vab-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 3px;
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
|
||||
@keyframes vabDot {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(2.4);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
animation: vabDot 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@mixin set-color($color) {
|
||||
&-#{$color} {
|
||||
background: var(--el-color-#{$color});
|
||||
|
||||
&::after {
|
||||
background: var(--el-color-#{$color});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include set-color(primary);
|
||||
@include set-color(success);
|
||||
@include set-color(warning);
|
||||
@include set-color(error);
|
||||
@include set-color(danger);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<el-table border :data="errorLogs">
|
||||
<el-table-column label="报错路由">
|
||||
<template #default="{ row }">
|
||||
<el-button :href="row.url" rel="noopener noreferrer" tag="a" target="_blank" text type="success">{{ row.url }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误信息">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger">{{ row.err.message }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.err.stack" effect="light">
|
||||
<el-button text type="primary">错误详情</el-button>
|
||||
</el-tooltip>
|
||||
<a>
|
||||
<el-button
|
||||
v-for="(item, index) in searchList"
|
||||
:key="index"
|
||||
:href="item.url + row.err.message"
|
||||
rel="noopener noreferrer"
|
||||
tag="a"
|
||||
target="_blank"
|
||||
text
|
||||
type="primary"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-button>
|
||||
</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty class="vab-data-empty" description="暂无数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useErrorLogStore } from '/@/store/modules/errorLog'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabErrorLogContent',
|
||||
})
|
||||
|
||||
const errorLogStore = useErrorLogStore()
|
||||
const { errorLogs } = storeToRefs(errorLogStore)
|
||||
|
||||
const searchList = ref<any>([
|
||||
{
|
||||
title: '百度搜索',
|
||||
url: 'https://www.baidu.com/baidu?wd=',
|
||||
icon: 'baidu-line',
|
||||
},
|
||||
{
|
||||
title: '谷歌搜索',
|
||||
url: 'https://www.google.com/search?q=',
|
||||
icon: 'google-line',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
33
library/components/VabErrorLog/index.vue
Normal file
33
library/components/VabErrorLog/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div v-if="errorLogs.length > 0">
|
||||
<el-badge type="danger" :value="errorLogs.length" @click="dialogVisible = true">
|
||||
<vab-icon icon="bug-line" />
|
||||
</el-badge>
|
||||
|
||||
<vab-dialog v-model="dialogVisible" append-to-body title="公寓项目异常捕获" width="60%">
|
||||
<vab-error-log-content />
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="danger" @click="clearAll">暂不显示</el-button>
|
||||
</template>
|
||||
</vab-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useErrorLogStore } from '/@/store/modules/errorLog'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabErrorLog',
|
||||
})
|
||||
|
||||
const errorLogStore = useErrorLogStore()
|
||||
const { errorLogs } = storeToRefs(errorLogStore)
|
||||
const { clearErrorLog } = errorLogStore
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const clearAll = () => {
|
||||
dialogVisible.value = false
|
||||
clearErrorLog()
|
||||
}
|
||||
</script>
|
||||
377
library/components/VabFallBar/index.vue
Normal file
377
library/components/VabFallBar/index.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div
|
||||
class="vab-fall-bar"
|
||||
:class="{
|
||||
'is-collapse': collapse,
|
||||
}"
|
||||
>
|
||||
<vab-logo style="z-index: 999" />
|
||||
<fall-menu :data="handleRoutes">
|
||||
<template #level1="{ slotScope }">
|
||||
<a :title="translate(slotScope.meta.title)" @click="handleLink(slotScope)">
|
||||
<vab-icon :icon="slotScope.meta && slotScope.meta.icon" />
|
||||
<span>{{ translate(slotScope.meta.title) }}</span>
|
||||
<vab-icon v-if="slotScope.children" class="fall-icon-right" icon="arrow-right-s-line" />
|
||||
</a>
|
||||
</template>
|
||||
<template #level2="{ slotScope }">
|
||||
<span style="cursor: pointer" @click="handleLink(slotScope)">
|
||||
<vab-icon :icon="slotScope.meta && slotScope.meta.icon" />
|
||||
<span>{{ translate(slotScope.meta.title) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #level3="{ slotScope }">
|
||||
<a v-for="(level3, index) in slotScope" :key="index" :href="level3.url" @click="handleLink(level3)">
|
||||
- {{ translate(level3.meta.title) }}
|
||||
<el-tag v-if="level3.meta && level3.meta.badge" effect="dark" size="small" type="danger">
|
||||
{{ level3.meta.badge }}
|
||||
</el-tag>
|
||||
<vab-dot v-if="level3.meta && level3.meta.dot" type="danger" />
|
||||
</a>
|
||||
</template>
|
||||
</fall-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* @author:zxwk-sundan
|
||||
* @description:瀑布菜单最多支持到三级菜单,不支持左侧点击选中,且非element-plus官方组件,生产环境请谨慎使用
|
||||
*/
|
||||
|
||||
import { FallMenu } from '@opentiny/vue'
|
||||
import { isHashRouterMode } from '/@/config'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabFallBar',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const routesStore = useRoutesStore()
|
||||
const { getRoutes: routes } = storeToRefs(routesStore)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const { device, collapse } = storeToRefs(settingsStore)
|
||||
const { foldSideBar } = settingsStore
|
||||
const { enter, exit } = useFullscreen()
|
||||
const mousePosition = ref({ x: 0, y: 0 })
|
||||
|
||||
const handleRoutes = computed(() =>
|
||||
routes.value.flatMap((route) => (route.meta.levelHidden && route.children ? [...route.children] : route))
|
||||
)
|
||||
|
||||
const handleLink = (slotScope) => {
|
||||
nextTick(() => {
|
||||
const routePath = slotScope.path
|
||||
const target = slotScope.meta.target
|
||||
const fullscreen = slotScope.meta.fullscreen
|
||||
|
||||
if (target === '_blank') {
|
||||
if (isExternal(routePath)) {
|
||||
window.open(routePath)
|
||||
router.push('/redirect')
|
||||
} else if (route.path !== routePath) isHashRouterMode ? window.open(`#${routePath}`) : window.open(routePath)
|
||||
router.push('/redirect')
|
||||
} else {
|
||||
if (isExternal(routePath)) window.location.href = routePath
|
||||
else if (route.path === routePath) {
|
||||
$pub('reload-router-view')
|
||||
} else {
|
||||
if (device.value === 'mobile') foldSideBar()
|
||||
if (slotScope.children) router.push(slotScope.redirect)
|
||||
else router.push(slotScope.path)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (fullscreen) enter()
|
||||
else exit()
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
useEventListener('mousemove', (e) => {
|
||||
mousePosition.value = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
}
|
||||
if ((mousePosition.value.x < 265 && !collapse.value) || (mousePosition.value.x < 65 && collapse.value)) {
|
||||
const element = document.querySelector('.vab-fall-bar .tiny-fall-menu__box')
|
||||
const base = 60
|
||||
const intervalSize = 48
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const lowerBound = base + i * intervalSize
|
||||
const upperBound = lowerBound + intervalSize
|
||||
if (mousePosition.value.y > lowerBound && mousePosition.value.y < upperBound) {
|
||||
mousePosition.value.y = lowerBound - 60
|
||||
break
|
||||
}
|
||||
}
|
||||
element.style.top = `${mousePosition.value.y}px`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-fall-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--el-z-index);
|
||||
width: calc(var(--el-left-menu-width) - 1px);
|
||||
background: var(--el-menu-background-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
|
||||
.fall-icon-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.tiny-fall-menu {
|
||||
--ti-fall-menu-bg-color-normal: var(--el-menu-background-color);
|
||||
--ti-fall-menu-bg-color-hover: var(--el-color-primary);
|
||||
--ti-fall-menu-slot-bg-color: var(--el-menu-background-color);
|
||||
--ti-fall-menu-box-text-color: var(--el-color-white);
|
||||
--ti-fall-menu-slot-text-color: var(--el-color-white);
|
||||
--ti-common-font-size-base: var(--el-font-size-base);
|
||||
--ti-fall-menu-title-font-size: var(--el-font-size-base);
|
||||
--ti-fall-menu-box-width: 560px;
|
||||
|
||||
&__nav {
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
padding: 0;
|
||||
background: var(--ti-fall-menu-bg-color-normal);
|
||||
}
|
||||
|
||||
&__subnav {
|
||||
.icon-slot-left,
|
||||
.icon-slot-right {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
right: 0 !important;
|
||||
left: 0 !important;
|
||||
min-width: 100%;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
width: calc(100% - 20px);
|
||||
margin: 0 10px 0px 10px !important;
|
||||
text-align: left;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
[class*='ri-'] {
|
||||
margin-left: 1.5px;
|
||||
}
|
||||
|
||||
[class*='ri-'] + span {
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fall-hide {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
top: 5px;
|
||||
left: var(--el-left-menu-width);
|
||||
min-width: var(--ti-fall-menu-box-width);
|
||||
padding: var(--el-padding);
|
||||
overflow-y: auto;
|
||||
border: 0;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-shadow: none;
|
||||
transition:
|
||||
all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||
top 0.1s !important;
|
||||
|
||||
.cont {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sublist {
|
||||
li {
|
||||
h3.mcate-item-hd {
|
||||
color: var(--ti-fall-menu-box-title-text-color);
|
||||
|
||||
[class*='ri-'] + span {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
p.mcate-item-bd {
|
||||
a {
|
||||
font-size: 13px;
|
||||
color: var(--ti-fall-menu-box-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
height: 16px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapse {
|
||||
width: 65px;
|
||||
|
||||
:deep() {
|
||||
.tiny-fall-menu {
|
||||
&__list {
|
||||
li {
|
||||
a {
|
||||
[class*='ri-'] + span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fall-hide {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
left: calc(var(--el-left-menu-width-min) + 2px);
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-theme-technology {
|
||||
.tiny-fall-menu {
|
||||
--ti-fall-menu-bg-color-normal: var(--el-menu-background-color);
|
||||
--ti-fall-menu-bg-color-hover: var(--el-color-primary);
|
||||
--ti-fall-menu-slot-bg-color: var(--el-menu-background-color);
|
||||
--ti-fall-menu-box-text-color: #fff !important;
|
||||
--ti-fall-menu-slot-text-color: var(--el-color-white);
|
||||
|
||||
&__list {
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
border: 1px solid var(--el-border-color) !important;
|
||||
|
||||
.sublist {
|
||||
li {
|
||||
h3.mcate-item-hd {
|
||||
color: var(--ti-fall-menu-box-title-text-color);
|
||||
}
|
||||
|
||||
p.mcate-item-bd {
|
||||
a {
|
||||
color: var(--ti-fall-menu-box-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vab-theme-plain {
|
||||
.tiny-fall-menu {
|
||||
--ti-fall-menu-bg-color-normal: var(--el-menu-background-color);
|
||||
--ti-fall-menu-bg-color-hover: var(--el-color-primary);
|
||||
--ti-fall-menu-slot-bg-color: var(--el-menu-background-color);
|
||||
--ti-fall-menu-box-text-color: var(--el-color-grey) !important;
|
||||
--ti-fall-menu-slot-text-color: var(--el-color-grey) !important;
|
||||
|
||||
&__box {
|
||||
border: 1px solid var(--el-border-color) !important;
|
||||
|
||||
.sublist {
|
||||
li {
|
||||
h3.mcate-item-hd {
|
||||
color: #515a6e !important;
|
||||
}
|
||||
|
||||
p.mcate-item-bd {
|
||||
a {
|
||||
color: var(--ti-fall-menu-box-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
a:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.tiny-fall-menu {
|
||||
--ti-fall-menu-bg-color-normal: var(--el-menu-background-color);
|
||||
--ti-fall-menu-bg-color-hover: var(--el-color-primary);
|
||||
--ti-fall-menu-slot-bg-color: var(--el-menu-background-color);
|
||||
--ti-fall-menu-box-text-color: var(--el-color-grey) !important;
|
||||
--ti-fall-menu-slot-text-color: var(--el-color-grey) !important;
|
||||
|
||||
&__box {
|
||||
border: 1px solid var(--el-border-color) !important;
|
||||
|
||||
.sublist {
|
||||
li {
|
||||
h3.mcate-item-hd {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
p.mcate-item-bd {
|
||||
a {
|
||||
color: var(--ti-fall-menu-box-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
a:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
library/components/VabFold/index.vue
Normal file
32
library/components/VabFold/index.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<vab-icon class="fold-unfold" :icon="collapse ? unfold : fold" @click="toggleCollapse" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabFold',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
unfold: {
|
||||
type: String,
|
||||
default: 'menu-unfold-line',
|
||||
},
|
||||
fold: {
|
||||
type: String,
|
||||
default: 'menu-fold-line',
|
||||
},
|
||||
})
|
||||
const settingsStore = useSettingsStore()
|
||||
const { collapse } = storeToRefs(settingsStore)
|
||||
const { toggleCollapse } = settingsStore
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fold-unfold {
|
||||
color: var(--el-color-grey);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
29
library/components/VabFontSize/index.vue
Normal file
29
library/components/VabFontSize/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<el-dropdown class="vab-language" @command="handleCommand">
|
||||
<vab-icon icon="font-size-2" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in fontSizeList" :key="item" :command="item">{{ item }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabFontSize',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme } = storeToRefs(settingsStore)
|
||||
const { updateTheme, saveTheme } = settingsStore
|
||||
const fontSizeList = ref(['13px', '13.5px', '14px', '15px', '15.5px', '16px'])
|
||||
|
||||
const handleCommand = (fontSize) => {
|
||||
theme.value.fontSize = fontSize
|
||||
updateTheme()
|
||||
saveTheme()
|
||||
}
|
||||
</script>
|
||||
63
library/components/VabFooter/index.vue
Normal file
63
library/components/VabFooter/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<footer v-if="theme?.showFooter" class="vab-footer">
|
||||
Copyright
|
||||
<vab-icon icon="copyright-line" />
|
||||
{{ fullYear }} {{ title }}
|
||||
|
||||
<a
|
||||
v-if="beian"
|
||||
class="hidden-xs-only"
|
||||
href="https://beian.miit.gov.cn/#/Integrated/index"
|
||||
style="margin-left: 3px; color: var(--el-color-grey)"
|
||||
target="_blank"
|
||||
>
|
||||
{{ beian }}
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabFooter',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const fullYear = new Date().getFullYear()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { title, theme } = storeToRefs(settingsStore)
|
||||
const beian = ref(localStorage.getItem('beian'))
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (route.query && route.query.beian) {
|
||||
beian.value = route.query.beian
|
||||
localStorage.setItem('beian', beian.value)
|
||||
} else {
|
||||
// 备案号
|
||||
if (location.hostname.includes('beautiful')) beian.value = ''
|
||||
// 官方站点
|
||||
if (location.hostname.includes('vuejs-core')) beian.value = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: var(--el-footer-height);
|
||||
padding: 0 var(--el-padding) 0 var(--el-padding);
|
||||
margin-top: var(--el-margin);
|
||||
color: var(--el-color-grey);
|
||||
background: var(--el-color-white);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
transition: var(--el-transition);
|
||||
|
||||
i {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
library/components/VabFullscreen/index.vue
Normal file
11
library/components/VabFullscreen/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<vab-icon class="vab-fullscreen" :icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'" @click="toggle" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'VabFullscreen',
|
||||
})
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
</script>
|
||||
132
library/components/VabHeader/index.vue
Normal file
132
library/components/VabHeader/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="vab-header">
|
||||
<div class="vab-main">
|
||||
<div class="right-panel">
|
||||
<vab-logo />
|
||||
<el-menu
|
||||
v-if="'horizontal' === layout"
|
||||
active-text-color="var(--el-menu-color-text)"
|
||||
background-color="var(--el-menu-background-color)"
|
||||
:default-active="activeMenu.data"
|
||||
menu-trigger="hover"
|
||||
mode="horizontal"
|
||||
text-color="var(--el-menu-color-text)"
|
||||
>
|
||||
<vab-menu v-for="(item, index) in handleRoutes" :key="index + item['name']" :item="item" :layout="layout" />
|
||||
</el-menu>
|
||||
<vab-right-tools is-horizontal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabHeader',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
},
|
||||
})
|
||||
|
||||
const routesStore = useRoutesStore()
|
||||
const { getActiveMenu: activeMenu, getRoutes: routes } = storeToRefs(routesStore)
|
||||
|
||||
const handleRoutes = computed(() =>
|
||||
routes.value.flatMap((route) => (route.meta && route.meta.levelHidden && route.children ? [...route.children] : route))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-header .vab-main .right-panel {
|
||||
.el-menu.el-menu--horizontal {
|
||||
width: calc(100vw * 0.92 - 195px - 435px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: flex-end;
|
||||
height: var(--el-header-height);
|
||||
background: var(--el-menu-background-color);
|
||||
|
||||
.vab-main {
|
||||
padding: 0 var(--el-padding) 0 var(--el-padding);
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--el-header-height);
|
||||
|
||||
:deep() {
|
||||
.vab-logo-horizontal {
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-more {
|
||||
margin-right: var(--el-margin) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__hide-arrow {
|
||||
.el-sub-menu__title {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
&.el-menu--horizontal {
|
||||
width: 60%;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
|
||||
* {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
> .el-menu-item {
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
&.is-active {
|
||||
background: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class*='ri-'] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.username,
|
||||
.username + i {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
|
||||
[class*='ri-'] {
|
||||
margin-left: var(--el-margin);
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.el-popper.is-pure.is-light:has(.el-menu--horizontal, .el-menu--popup-container) {
|
||||
margin-top: calc(var(--el-margin) * 0.4);
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
35
library/components/VabLink/index.vue
Normal file
35
library/components/VabLink/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps()">
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabLink',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const type = computed(() => (isExternal(props.to) ? 'a' : 'router-link'))
|
||||
|
||||
const linkProps = () =>
|
||||
isExternal(props.to)
|
||||
? {
|
||||
href: props.to,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
}
|
||||
: { to: props.to, target: props.target }
|
||||
</script>
|
||||
235
library/components/VabLock/index.vue
Normal file
235
library/components/VabLock/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="vab-lock">
|
||||
<vab-icon icon="lock-2-line" @click="handleLock" />
|
||||
<el-drawer
|
||||
v-model="lock"
|
||||
append-to-body
|
||||
class="vab-lock-drawer"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
direction="ttb"
|
||||
:show-close="false"
|
||||
size="100%"
|
||||
:with-header="false"
|
||||
>
|
||||
<div class="vab-screen-lock">
|
||||
<div id="vab-screen-lock-background" class="vab-screen-lock-background" :style="style"></div>
|
||||
<div class="vab-screen-lock-content">
|
||||
<div class="vab-screen-lock-content-title">
|
||||
<el-avatar :size="180" :src="avatar" />
|
||||
<vab-icon icon="lock-2-line" />
|
||||
{{ title }} {{ translate('屏幕已锁定') }}
|
||||
</div>
|
||||
<div class="vab-screen-lock-content-form">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" v-focus autocomplete="off" :placeholder="translate('请输入密码123456')" type="password" />
|
||||
<el-button native-type="submit" type="primary" @click="handleUnLock">
|
||||
<vab-icon icon="rotate-lock-2-line" />
|
||||
<span>{{ translate('解锁') }}</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { translate } from '/@/i18n'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { useUserStore } from '/@/store/modules/user'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabLock',
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { avatar } = storeToRefs(userStore)
|
||||
const settingsStore = useSettingsStore()
|
||||
const { lock, title } = storeToRefs(settingsStore)
|
||||
const { handleLock: _handleLock, handleUnLock: _handleUnLock } = settingsStore
|
||||
const url = 'https://cdn.jsdelivr.net/gh/chuzhixin/image/vab-image-lock/'
|
||||
const background = ref(`${url}${Math.round(Math.random() * 31)}.jpg`)
|
||||
|
||||
const style = reactive({
|
||||
background: 'var(--el-color-primary-light-5)',
|
||||
backgroundSize: '100%',
|
||||
filter: 'blur(5px)',
|
||||
transform: 'scale(1.05)',
|
||||
transition: 'all 3s ease-in-out',
|
||||
})
|
||||
|
||||
const validatePass = (rule, value, callback) => {
|
||||
if (value === '' || value !== '123456') {
|
||||
callback(new Error('请输入正确的密码'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
password: '123456',
|
||||
})
|
||||
const rules = {
|
||||
password: [{ validator: validatePass, trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const handleUnLock = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) await _handleUnLock()
|
||||
})
|
||||
}
|
||||
|
||||
const handleLock = () => {
|
||||
_handleLock()
|
||||
}
|
||||
|
||||
watch(
|
||||
lock,
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
lock.value ? (style.transform = 'scale(1.2)') : (style.transform = 'scale(1.05)')
|
||||
}, 500)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-overlay:has(.vab-lock-drawer) {
|
||||
backdrop-filter: none;
|
||||
|
||||
.vab-lock-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-lock-drawer {
|
||||
.vab-screen-lock {
|
||||
position: relative;
|
||||
z-index: var(--el-z-index);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
font-weight: bold;
|
||||
background: var(--el-mask-color);
|
||||
opacity: var(--opacity-value);
|
||||
|
||||
&-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: calc(var(--el-z-index) - 1);
|
||||
}
|
||||
|
||||
&-content {
|
||||
z-index: var(--el-z-index);
|
||||
width: 400px;
|
||||
padding: 40px 55px 40px 55px;
|
||||
color: var(--el-color-grey);
|
||||
text-align: center;
|
||||
background: var(--el-mask-color);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 15px;
|
||||
|
||||
> span {
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-title {
|
||||
line-height: 50px;
|
||||
color: var(--el-color-grey);
|
||||
text-align: center;
|
||||
|
||||
:deep() {
|
||||
.el-avatar {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
|
||||
img {
|
||||
padding: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
[class*='ri-'] {
|
||||
display: block;
|
||||
margin: auto !important;
|
||||
font-size: 30px;
|
||||
color: var(--el-color-grey) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-form {
|
||||
:deep() {
|
||||
.el-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
|
||||
&__wrapper {
|
||||
padding-right: 0;
|
||||
border: 1px solid var(--el-color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&__suffix {
|
||||
.el-input__validateIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
z-index: 999;
|
||||
height: 40px;
|
||||
margin-left: 0 !important;
|
||||
line-height: 40px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
[class*='ri-'] {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.vab-screen-lock-content {
|
||||
width: 100% !important;
|
||||
padding: 40px 35px 40px 35px;
|
||||
margin: 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
library/components/VabLogo/index.vue
Normal file
121
library/components/VabLogo/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="vab-logo" :class="{ ['vab-logo-' + theme.layout]: true }">
|
||||
<router-link to="/">
|
||||
<span class="logo">
|
||||
<img src="../../../public/logo.png" alt="" style="width: 40px; height: 40px;">
|
||||
</span>
|
||||
<span class="title" :class="{ 'hidden-xs-only': theme.layout === 'horizontal' }"> {{ title }} </span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabLogo',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme, logo, title } = storeToRefs(settingsStore)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin container {
|
||||
position: relative;
|
||||
height: var(--el-header-height);
|
||||
overflow: hidden;
|
||||
line-height: var(--el-header-height);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@mixin logo {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--el-title-color);
|
||||
vertical-align: middle;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@mixin title {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-extra-large);
|
||||
line-height: 55px;
|
||||
color: var(--el-title-color);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vab-logo {
|
||||
&-horizontal {
|
||||
@include container;
|
||||
|
||||
.logo {
|
||||
svg,
|
||||
img {
|
||||
@include logo;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@include title;
|
||||
}
|
||||
}
|
||||
|
||||
&-vertical,
|
||||
&-column,
|
||||
&-comprehensive,
|
||||
&-fall {
|
||||
@include container;
|
||||
|
||||
height: var(--el-logo-height);
|
||||
line-height: var(--el-logo-height);
|
||||
text-align: center;
|
||||
|
||||
.logo {
|
||||
svg,
|
||||
img {
|
||||
@include logo;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@include title;
|
||||
max-width: calc(var(--el-left-menu-width) - 60);
|
||||
}
|
||||
}
|
||||
|
||||
&-column {
|
||||
background: var(--el-color-white) !important;
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: var(--el-left-menu-width-min);
|
||||
height: var(--el-logo-height);
|
||||
margin: 0;
|
||||
background: var(--el-menu-background-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
position: fixed;
|
||||
left: var(--el-left-menu-width-min) !important;
|
||||
box-sizing: border-box;
|
||||
display: block !important;
|
||||
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min) - 1px);
|
||||
height: var(--el-nav-height);
|
||||
margin-left: 0 !important;
|
||||
color: var(--el-color-grey) !important;
|
||||
background: var(--el-color-white) !important;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
@include title;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
87
library/components/VabMenu/components/VabMenuItem.vue
Normal file
87
library/components/VabMenu/components/VabMenuItem.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<el-menu-item :index="itemOrMenu.path" @click="handleLink">
|
||||
<vab-icon
|
||||
v-if="itemOrMenu.meta && itemOrMenu.meta.icon"
|
||||
:icon="itemOrMenu.meta.icon"
|
||||
:is-custom-svg="itemOrMenu.meta.isCustomSvg"
|
||||
:title="translate(itemOrMenu.meta.title)"
|
||||
/>
|
||||
<span :title="translate(itemOrMenu.meta.title)">
|
||||
{{ translate(itemOrMenu.meta.title) }}
|
||||
</span>
|
||||
<el-tag v-if="itemOrMenu.meta && itemOrMenu.meta.badge" effect="dark" type="danger">
|
||||
{{ translate(itemOrMenu.meta.badge) }}
|
||||
</el-tag>
|
||||
<vab-dot v-if="itemOrMenu.meta && itemOrMenu.meta.dot" type="danger" />
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isHashRouterMode } from '/@/config'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabMenuItem',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
itemOrMenu: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { device } = storeToRefs(settingsStore)
|
||||
const { foldSideBar } = settingsStore
|
||||
const { enter, exit } = useFullscreen()
|
||||
|
||||
const handleLink = () => {
|
||||
nextTick(() => {
|
||||
const routePath = props.itemOrMenu.path
|
||||
const target = props.itemOrMenu.meta.target
|
||||
const fullscreen = props.itemOrMenu.meta.fullscreen
|
||||
|
||||
if (target === '_blank') {
|
||||
if (isExternal(routePath)) {
|
||||
window.open(routePath)
|
||||
router.push('/redirect')
|
||||
} else if (route.path !== routePath) isHashRouterMode ? window.open(`#${routePath}`) : window.open(routePath)
|
||||
router.push('/redirect')
|
||||
} else {
|
||||
if (isExternal(routePath)) window.location.href = routePath
|
||||
else if (route.path !== routePath) {
|
||||
if (device.value === 'mobile') foldSideBar()
|
||||
router.push(props.itemOrMenu.path)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (fullscreen) enter()
|
||||
else exit()
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tag) {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
height: 18px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.vab-dot {
|
||||
position: absolute !important;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
36
library/components/VabMenu/components/VabSubMenu.vue
Normal file
36
library/components/VabMenu/components/VabSubMenu.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<template v-if="itemOrMenu.meta && itemOrMenu.meta.levelHidden">
|
||||
<template v-for="route in itemOrMenu.children" :key="route.path">
|
||||
<vab-menu :item="route" />
|
||||
</template>
|
||||
</template>
|
||||
<el-sub-menu v-else :index="itemOrMenu.path">
|
||||
<template #title>
|
||||
<vab-icon
|
||||
v-if="itemOrMenu.meta && itemOrMenu.meta.icon"
|
||||
:icon="itemOrMenu.meta.icon"
|
||||
:is-custom-svg="itemOrMenu.meta.isCustomSvg"
|
||||
:title="translate(itemOrMenu.meta.title)"
|
||||
/>
|
||||
<span :title="translate(itemOrMenu.meta.title)">
|
||||
{{ translate(itemOrMenu.meta.title) }}
|
||||
</span>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { translate } from '/@/i18n'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabSubMenu',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
itemOrMenu: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
39
library/components/VabMenu/index.vue
Normal file
39
library/components/VabMenu/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<component :is="menuComponent" :item-or-menu="item">
|
||||
<template v-if="item.children && item.children.length > 0">
|
||||
<vab-menu v-for="route in item.children" :key="route.path" :item="route" />
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'VabMenu',
|
||||
})
|
||||
|
||||
const imports = import.meta.glob('./**/*.vue', { eager: true })
|
||||
const Components = {}
|
||||
Object.getOwnPropertyNames(imports).forEach((key) => {
|
||||
Components[key.replaceAll(/(\/|components|\.|vue)/g, '')] = imports[key].default
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const menuComponent = computed(() =>
|
||||
props.item.children &&
|
||||
props.item.children.some((route) => {
|
||||
return route.meta && route.meta.hidden !== true
|
||||
})
|
||||
? Components['VabSubMenu']
|
||||
: Components['VabMenuItem']
|
||||
)
|
||||
</script>
|
||||
165
library/components/VabNav/index.vue
Normal file
165
library/components/VabNav/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="vab-nav">
|
||||
<div class="left-panel">
|
||||
<vab-fold fold="contract-left-line" unfold="contract-right-line" />
|
||||
<el-tabs
|
||||
v-if="layout === 'comprehensive'"
|
||||
v-model="tab.data"
|
||||
class="comprehensive-tabs"
|
||||
tab-position="top"
|
||||
@tab-click="handleTabClick"
|
||||
>
|
||||
<template v-for="item in routes" :key="item.name">
|
||||
<el-tab-pane :name="item.name">
|
||||
<template #label>
|
||||
<vab-icon v-if="item.meta.icon" :icon="item.meta.icon" :is-custom-svg="item.meta.isCustomSvg" />
|
||||
{{ translate(item.meta.title) }}
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
</el-tabs>
|
||||
<vab-breadcrumb v-else class="hidden-xs-only hidden-md-and-down" />
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<vab-right-tools />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { openFirstMenu } from '/@/config'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabNav',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
layout: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const routesStore = useRoutesStore()
|
||||
const { getTab: tab, getTabMenu: tabMenu, getRoutes: routes } = storeToRefs(routesStore)
|
||||
|
||||
const handleTabClick = () => {
|
||||
nextTick(() => {
|
||||
if (isExternal(tabMenu.value.path)) {
|
||||
window.open(tabMenu.value.path)
|
||||
router.push('/redirect')
|
||||
} else if (openFirstMenu) router.push(tabMenu.value.redirect || tabMenu.value)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-layout-comprehensive {
|
||||
.comprehensive-tabs {
|
||||
width: calc(100vw - var(--el-left-menu-width) - 675px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.vab-nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: var(--el-nav-height);
|
||||
padding-right: var(--el-padding);
|
||||
padding-left: var(--el-padding);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
background: var(--el-color-white);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
height: var(--el-nav-height);
|
||||
|
||||
:deep() {
|
||||
.fold-unfold {
|
||||
margin-right: var(--el-margin);
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
|
||||
> .el-tabs__nav-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon-arrow-left,
|
||||
.el-icon-arrow-right {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: var(--el-nav-height);
|
||||
transition: var(--el-transition);
|
||||
|
||||
:deep() {
|
||||
[class*='ri-'] {
|
||||
margin-left: var(--el-margin);
|
||||
color: var(--el-color-grey);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
[class*='ri-'] {
|
||||
margin-left: 0;
|
||||
color: var(--el-color-white);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.right-panel {
|
||||
:deep() {
|
||||
.el-badge,
|
||||
.ri-refresh-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
library/components/VabNotice/index.vue
Normal file
133
library/components/VabNotice/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<el-badge type="danger" :value="badge">
|
||||
<el-popover placement="bottom" trigger="hover" :width="305">
|
||||
<template #reference>
|
||||
<vab-icon icon="notification-2-line" />
|
||||
</template>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane :label="translate('通知')" name="notice">
|
||||
<div class="notice-list">
|
||||
<el-scrollbar>
|
||||
<ul v-if="badge">
|
||||
<li v-for="(item, index) in notices" :key="index">
|
||||
<el-avatar :size="45" :src="item.image" />
|
||||
<span v-html="item.notice"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="translate('邮件')" name="email">
|
||||
<div class="notice-list">
|
||||
<el-scrollbar>
|
||||
<ul v-if="badge">
|
||||
<li v-for="(item, index) in notices" :key="index">
|
||||
<el-avatar :size="45" :src="item.image" />
|
||||
<span>{{ item.email }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="notice-clear" @click="handleClearNotice">
|
||||
<el-button text>
|
||||
<vab-icon icon="close-circle-line" />
|
||||
<span>{{ translate('清空消息') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-badge>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { translate } from '/@/i18n'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabNotice',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme } = storeToRefs(settingsStore)
|
||||
const activeName = ref('notice')
|
||||
const notices = ref([])
|
||||
const badge = ref(undefined)
|
||||
|
||||
const fetchData = async () => {
|
||||
// const { data } = await getList()
|
||||
// data.list
|
||||
notices.value = []
|
||||
// badge.value = data.total === 0 ? undefined : data.total
|
||||
badge.value = 0
|
||||
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleClearNotice = () => {
|
||||
badge.value = ''
|
||||
notices.value = []
|
||||
$baseMessage('清空消息成功', 'success', 'hey')
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (theme.value.showNotice) fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.el-tabs__active-bar {
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
height: 315px;
|
||||
|
||||
ul {
|
||||
padding: 0 15px 0 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0 15px 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.el-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0 0 0;
|
||||
font-size: var(--el-font-size-base);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
}
|
||||
</style>
|
||||
44
library/components/VabPagination/index.vue
Normal file
44
library/components/VabPagination/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<el-pagination
|
||||
:background="background"
|
||||
:current-page="currentPage"
|
||||
:default-current-page="defaultCurrentPage"
|
||||
:default-page-size="defaultPageSize"
|
||||
:disabled="disabled"
|
||||
:hide-on-single-page="hideOnSinglePage"
|
||||
:layout="layout"
|
||||
:next-icon="nextIcon"
|
||||
:next-text="nextText"
|
||||
:page-count="pageCount"
|
||||
:page-size="PageSize"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:popper-class="popperClass"
|
||||
:prev-icon="prevIcon"
|
||||
:prev-text="prevText"
|
||||
:small="small"
|
||||
:teleported="teleported"
|
||||
:total="total"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElPagination } from 'element-plus';
|
||||
|
||||
defineOptions({
|
||||
name: 'VabPagination',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
...ElPagination.props,
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'total, sizes, prev, pager, next, jumper',
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<el-col :span="24">
|
||||
<div class="bottom-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
|
||||
<div class="left-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
span: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
|
||||
<div class="right-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
span: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<el-col :span="24">
|
||||
<div class="top-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
70
library/components/VabQueryForm/index.vue
Normal file
70
library/components/VabQueryForm/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<el-row class="vab-query-form" :gutter="0">
|
||||
<slot></slot>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'VabQueryForm',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: var(-el-input-height);
|
||||
margin: 0 0 calc(var(--el-margin) / 2) 0;
|
||||
.el-form-item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> .el-button {
|
||||
margin: 0 10px calc(var(--el-margin) / 2) 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vab-query-form {
|
||||
:deep() {
|
||||
.el-input,
|
||||
.el-select {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.el-form-item:first-child {
|
||||
margin: 0 0 calc(var(--el-margin) / 2) 0 !important;
|
||||
}
|
||||
|
||||
.el-form-item + .el-form-item {
|
||||
margin: 0 0 calc(var(--el-margin) / 2) 0 !important;
|
||||
|
||||
.el-button {
|
||||
margin: 0 0 0 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top-panel {
|
||||
@include panel;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
@include panel;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
@include panel;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
@include panel;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
library/components/VabRefresh/index.vue
Normal file
29
library/components/VabRefresh/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<vab-icon :class="className" icon="refresh-line" @click="refreshRoute" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'VabRefresh',
|
||||
})
|
||||
|
||||
const className = ref('')
|
||||
|
||||
const rotate = () => {
|
||||
className.value = 'rotate'
|
||||
setTimeout(() => {
|
||||
className.value = ''
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const refreshRoute = () => {
|
||||
$pub('reload-router-view')
|
||||
rotate()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
$sub('refresh-rotate', () => {
|
||||
rotate()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
89
library/components/VabRightTools/index.vue
Normal file
89
library/components/VabRightTools/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="vab-right-tools">
|
||||
<vab-search v-show="!isHorizontal" class="hidden-xs-only" />
|
||||
<div class="vab-right-tools-draggable">
|
||||
<vab-dark v-show="theme.showDark" :style="!isHorizontal ? '' : { marginLeft: 'var(--el-margin)' }" />
|
||||
<vab-color-picker v-show="theme.showColorPicker" />
|
||||
<vab-error-log class="hidden-xs-only" />
|
||||
<vab-font-size v-show="theme.showFontSize" />
|
||||
<vab-lock v-show="theme.showLock" />
|
||||
<vab-notice v-show="theme.showNotice" />
|
||||
<vab-fullscreen v-show="theme.showFullScreen" />
|
||||
<vab-refresh v-show="theme.showRefresh" />
|
||||
</div>
|
||||
<vab-avatar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Sortable from 'sortablejs'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabRightTools',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
isHorizontal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme, device } = storeToRefs(settingsStore)
|
||||
const routeName = ref(route.name)
|
||||
|
||||
let sortable
|
||||
const handleTabDrag = () => {
|
||||
if (theme.value.rightToolsDrag && device.value != 'mobile') {
|
||||
const toolsElement = document.querySelector('.vab-right-tools-draggable')
|
||||
|
||||
if (toolsElement)
|
||||
sortable = new Sortable(toolsElement, {
|
||||
animation: 150,
|
||||
easing: 'cubic-bezier(1, 0, 0, 1)',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
routeName.value = route.name
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
handleTabDrag()
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
theme.value,
|
||||
() => {
|
||||
if (theme.value.rightToolsDrag) handleTabDrag()
|
||||
else sortable && sortable.destroy()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-right-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&-draggable {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
72
library/components/VabRouterView/index.vue
Normal file
72
library/components/VabRouterView/index.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition mode="out-in" :name="theme.pageTransition">
|
||||
<keep-alive :include="keepAliveNameList" :max="keepAliveMaxNum">
|
||||
<component :is="Component" :key="routerKey" ref="componentRef" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useHead } from '@vueuse/head'
|
||||
import VabProgress from 'nprogress'
|
||||
import { keepAliveMaxNum } from '/@/config'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { useTabsStore } from '/@/store/modules/tabs'
|
||||
import { handleActivePath } from '/@/utils/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabRouterView',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme } = storeToRefs(settingsStore)
|
||||
const tabsStore = useTabsStore()
|
||||
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
|
||||
const componentRef = ref()
|
||||
const routerKey = ref()
|
||||
const keepAliveNameList = ref()
|
||||
const siteData = reactive({
|
||||
description: '',
|
||||
})
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{
|
||||
name: `description`,
|
||||
content: computed(() => siteData.description),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const updateKeepAliveNameList = (refreshRouteName = null) => {
|
||||
keepAliveNameList.value = visitedRoutes.value
|
||||
.filter((item) => !item.meta.noKeepAlive && item.name !== refreshRouteName)
|
||||
.flatMap((item) => item.name)
|
||||
}
|
||||
|
||||
// 更新KeepAlive缓存页面
|
||||
watchEffect(() => {
|
||||
routerKey.value = handleActivePath(route, true)
|
||||
updateKeepAliveNameList()
|
||||
siteData.description = `迪联科技`
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
$sub('reload-router-view', (refreshRouteName = route.name) => {
|
||||
if (theme.value.showProgressBar) VabProgress.start()
|
||||
const cacheActivePath = routerKey.value
|
||||
routerKey.value = null
|
||||
updateKeepAliveNameList(refreshRouteName)
|
||||
nextTick(() => {
|
||||
routerKey.value = cacheActivePath
|
||||
updateKeepAliveNameList()
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (theme.value.showProgressBar) VabProgress.done()
|
||||
}, 200)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
93
library/components/VabSearch/index.vue
Normal file
93
library/components/VabSearch/index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<el-tree-select
|
||||
v-if="theme.showSearch"
|
||||
v-model="searchValue"
|
||||
class="vab-search"
|
||||
clearable
|
||||
:data="addFieldToTree(routes)"
|
||||
default-expand-all
|
||||
filterable
|
||||
highlight-current
|
||||
:prefix-icon="Search"
|
||||
@node-click="handleSelect"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<vab-icon v-if="data.meta && data.meta.icon" :icon="data.meta.icon" />
|
||||
<span style="margin-left: 3px">{{ translate(data.meta.title) }}</span>
|
||||
</template>
|
||||
</el-tree-select>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { isHashRouterMode } from '/@/config'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { isExternal } from '/@/utils/validate'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabSearch',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme } = storeToRefs(settingsStore)
|
||||
const searchValue = ref('')
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const routesStore = useRoutesStore()
|
||||
const { getRoutes: routes } = storeToRefs(routesStore)
|
||||
|
||||
const addFieldToTree = (routes) => {
|
||||
routes.forEach((node) => {
|
||||
node.value = node.name
|
||||
node.label = translate(node.meta.title)
|
||||
if (node.children && node.children.length > 0) addFieldToTree(node.children)
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
const handleSelect = (item) => {
|
||||
nextTick(() => {
|
||||
if (!item.children)
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
router.push('/redirect')
|
||||
return
|
||||
} else if (item.meta.target === '_blank') {
|
||||
isHashRouterMode ? window.open(`#${item.path}`) : window.open(item.path)
|
||||
router.push('/redirect')
|
||||
return
|
||||
} else router.push(item.path)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
if (route.fullPath.includes('?')) {
|
||||
//处理query传参
|
||||
const matched = route.fullPath.match(/\?(.*)$/)
|
||||
const name = route.name
|
||||
if (matched) name.includes('?') ? (searchValue.value = route.name) : (searchValue.value = `${route.name}?${matched[1]}`)
|
||||
// 详情页显示搜索项
|
||||
if (route.meta.hidden && name.includes('Detail')) searchValue.value = ''
|
||||
} else searchValue.value = route.name
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-search {
|
||||
margin-left: var(--el-margin);
|
||||
|
||||
:deep() {
|
||||
.el-input {
|
||||
width: 150px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
library/components/VabSideBar/index.vue
Normal file
165
library/components/VabSideBar/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<el-scrollbar class="vab-side-bar" :class="{ 'is-collapse': collapse }">
|
||||
<vab-logo v-if="layout === 'comprehensive' || layout === 'vertical'" class="fixed-logo" />
|
||||
<el-menu
|
||||
background-color="var(--el-menu-background-color)"
|
||||
:collapse="collapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="activeMenu.data"
|
||||
:default-openeds="defaultOpeneds"
|
||||
menu-trigger="click"
|
||||
mode="vertical"
|
||||
text-color="var(--el-menu-color-text)"
|
||||
:unique-opened="uniqueOpened"
|
||||
>
|
||||
<vab-menu v-for="(item, index) in handleRoutes" :key="index + item.name" :item="item" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defaultOpeneds, uniqueOpened } from '/@/config'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabSideBar',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'vertical',
|
||||
},
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { collapse } = storeToRefs(settingsStore)
|
||||
const routesStore = useRoutesStore()
|
||||
const { getRoutes: routes, getActiveMenu: activeMenu, getPartialRoutes: partialRoutes } = storeToRefs(routesStore)
|
||||
|
||||
const handleRoutes = computed(() =>
|
||||
props.layout === 'comprehensive'
|
||||
? partialRoutes.value
|
||||
: routes.value.flatMap((route) => (route.meta.levelHidden && route.children ? [...route.children] : route))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin active {
|
||||
&:hover {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.vab-side-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--el-left-menu-width);
|
||||
overflow: hidden;
|
||||
background: var(--el-menu-background-color);
|
||||
transition: var(--el-transition);
|
||||
|
||||
.fixed-logo {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--el-z-index);
|
||||
width: 100%;
|
||||
height: var(--el-header-height);
|
||||
background: var(--el-menu-background-color);
|
||||
}
|
||||
|
||||
&.is-collapse {
|
||||
z-index: calc(var(--el-z-index) + 1);
|
||||
width: var(--el-left-menu-width-min);
|
||||
border-right: 0;
|
||||
|
||||
:deep() {
|
||||
.el-menu {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
|
||||
.el-menu--collapse.el-menu {
|
||||
> .el-menu-item,
|
||||
> .el-sub-menu .el-sub-menu__title {
|
||||
justify-content: center;
|
||||
height: calc(var(--el-menu-item-height) - 6px);
|
||||
padding: 0;
|
||||
line-height: calc(var(--el-menu-item-height) - 6px);
|
||||
text-align: center;
|
||||
|
||||
[class*='ri'] {
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
border-right: 0;
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
right: 10px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.el-menu.el-menu--vertical {
|
||||
margin-top: var(--el-header-height);
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: var(--el-menu-item-height);
|
||||
margin: 0 10px 5px 10px;
|
||||
overflow: hidden;
|
||||
line-height: var(--el-menu-item-height);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
@include active;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.el-menu--popup-right-start {
|
||||
--el-menu-hover-bg-color: var(--el-color-primary) !important;
|
||||
--el-menu-active-color: var(--el-color-white) !important;
|
||||
|
||||
.is-active {
|
||||
background: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
library/components/VabStatistics/index.vue
Normal file
36
library/components/VabStatistics/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
defineOptions({
|
||||
name: 'VabStatistics',
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (location.hostname !== 'localhost' && !location.hostname.includes('127') && !location.hostname.includes('192')) {
|
||||
;(function () {
|
||||
const hm = document.createElement('script')
|
||||
let k = '820b686671af452e8a4e18952ce946d8'
|
||||
if (location.hostname.includes('vuejs-core')) k = '9578a46b371ba85ee55bc868d6b30692'
|
||||
hm.src = `//hm.baidu.com/hm.js?${k}`
|
||||
const s = document.querySelectorAll('script')[0]
|
||||
s.parentNode.insertBefore(hm, s)
|
||||
})()
|
||||
;(function (c, l, a, r, i, t, y) {
|
||||
c[a] =
|
||||
c[a] ||
|
||||
function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
;(c[a].q = c[a].q || []).push(arguments)
|
||||
}
|
||||
t = l.createElement(r)
|
||||
t.async = 1
|
||||
t.src = `//www.clarity.ms/tag/${i}`
|
||||
y = l.getElementsByTagName(r)[0]
|
||||
y.parentNode.insertBefore(t, y)
|
||||
})(window, document, 'clarity', 'script', 'j9de7dmm7n')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
59
library/components/VabTabs/components/VabTabsSetting.vue
Normal file
59
library/components/VabTabs/components/VabTabsSetting.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<vab-dialog v-model="drawerVisible" append-to-body :title="translate('标签设置')" width="400px">
|
||||
<el-form ref="form" label-position="top" :model="theme">
|
||||
<el-form-item v-if="theme.showTabs" :label="translate('标签风格')">
|
||||
<el-radio-group v-model="theme.tabsBarStyle">
|
||||
<el-radio-button v-for="item in tabsBarStyleList" :key="item.value" :label="translate(item.label)" :value="item.value" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="translate('标签图标')">
|
||||
<el-switch v-model="theme.showTabsIcon" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="translate('持久化标签')">
|
||||
<el-switch v-model="persistenceTab" @change="handlePersistenceTab" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSaveTheme">
|
||||
{{ translate('保存') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</vab-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { translate } from '/@/i18n'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabTabsSetting',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme, persistenceTab } = storeToRefs(settingsStore)
|
||||
const { saveTheme, updateCaughtTabs } = settingsStore
|
||||
const drawerVisible = ref<boolean>(false)
|
||||
const tabsBarStyleList = ref<any>([
|
||||
{ label: '卡片', value: 'card' },
|
||||
{ label: '灵动', value: 'smart' },
|
||||
{ label: '圆滑', value: 'smooth' },
|
||||
{ label: '矩形', value: 'rect' },
|
||||
])
|
||||
|
||||
const handleOpenSetting = () => {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleOpenSetting,
|
||||
})
|
||||
|
||||
const handlePersistenceTab = (value: any) => {
|
||||
updateCaughtTabs(value)
|
||||
}
|
||||
|
||||
const handleSaveTheme = () => {
|
||||
saveTheme()
|
||||
drawerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
695
library/components/VabTabs/index.vue
Normal file
695
library/components/VabTabs/index.vue
Normal file
@@ -0,0 +1,695 @@
|
||||
<template>
|
||||
<div class="vab-tabs">
|
||||
<div class="vab-tabs-draggable">
|
||||
<el-tabs
|
||||
v-model="tabActive"
|
||||
class="vab-tabs-content"
|
||||
:class="{ ['vab-tabs-content-' + theme.tabsBarStyle]: true }"
|
||||
@tab-click="handleTabClick"
|
||||
@tab-remove="handleTabRemove"
|
||||
>
|
||||
<el-tab-pane v-for="item in visitedRoutes" :key="item" :closable="!isNoClosable(item)" :name="item.path">
|
||||
<template #label>
|
||||
<span class="vab-tabs-title" @contextmenu.prevent="openMenu(item)">
|
||||
<template v-if="theme.showTabsIcon">
|
||||
<vab-icon v-if="item.meta && item.meta.icon" :icon="item.meta.icon" :is-custom-svg="item.meta.isCustomSvg" />
|
||||
<vab-icon v-else :icon="item.parentIcon" />
|
||||
</template>
|
||||
<span v-if="!isNoClosable(item)" @dblclick="handleTabRemove(item.path)">
|
||||
{{ translate(item.meta.title) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ translate(item.meta.title) }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
placement="bottom-end"
|
||||
popper-class="vab-tabs-more-dropdown"
|
||||
@command="handleCommand"
|
||||
@visible-change="handleVisibleChange"
|
||||
>
|
||||
<span class="vab-tabs-more" :class="{ 'vab-tabs-more-active': active }">
|
||||
<span class="vab-tabs-more-icon">
|
||||
<i class="box box-t"></i>
|
||||
<i class="box box-b"></i>
|
||||
</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="tabs-more">
|
||||
<el-dropdown-item command="refresh">
|
||||
<vab-icon icon="refresh-line" />
|
||||
<span>
|
||||
{{ translate('刷新') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeOthersTabs">
|
||||
<vab-icon icon="close-line" />
|
||||
<span>
|
||||
{{ translate('关闭其他') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeLeftTabs">
|
||||
<vab-icon icon="arrow-left-line" />
|
||||
<span>
|
||||
{{ translate('关闭左侧') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeRightTabs">
|
||||
<vab-icon icon="arrow-right-line" />
|
||||
<span>
|
||||
{{ translate('关闭右侧') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeAllTabs">
|
||||
<vab-icon icon="close-line" />
|
||||
<span>
|
||||
{{ translate('关闭全部') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="setting">
|
||||
<vab-icon icon="settings-5-line" />
|
||||
<span>
|
||||
{{ translate('标签设置') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<ul v-if="visible" class="contextmenu el-dropdown-menu" :style="{ left: left + 'px', top: top + 'px' }">
|
||||
<li class="el-dropdown-menu__item" @click="refresh">
|
||||
<vab-icon icon="refresh-line" />
|
||||
<span>{{ translate('刷新') }}</span>
|
||||
</li>
|
||||
<li class="el-dropdown-menu__item" :class="{ 'is-disabled': visitedRoutes.length === 1 }" @click="closeOthersTabs">
|
||||
<vab-icon icon="close-line" />
|
||||
<span>{{ translate('关闭其他') }}</span>
|
||||
</li>
|
||||
<li class="el-dropdown-menu__item" :class="{ 'is-disabled': !visitedRoutes.indexOf(hoverRoute) }" @click="closeLeftTabs">
|
||||
<vab-icon icon="arrow-left-line" />
|
||||
<span>{{ translate('关闭左侧') }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="el-dropdown-menu__item"
|
||||
:class="{
|
||||
'is-disabled': visitedRoutes.indexOf(hoverRoute) === visitedRoutes.length - 1,
|
||||
}"
|
||||
@click="closeRightTabs"
|
||||
>
|
||||
<vab-icon icon="arrow-right-line" />
|
||||
<span>{{ translate('关闭右侧') }}</span>
|
||||
</li>
|
||||
<li class="el-dropdown-menu__item" @click="closeAllTabs">
|
||||
<vab-icon icon="close-line" />
|
||||
<span>{{ translate('关闭全部') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<vab-tabs-setting ref="tabsSettingRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Sortable from 'sortablejs'
|
||||
import { translate } from '/@/i18n'
|
||||
import { useRoutesStore } from '/@/store/modules/routes'
|
||||
import { useSettingsStore } from '/@/store/modules/settings'
|
||||
import { useTabsStore } from '/@/store/modules/tabs'
|
||||
import { moveElement } from '/@/utils/index'
|
||||
import { handleActivePath, handleTabs } from '/@/utils/routes'
|
||||
|
||||
defineOptions({
|
||||
name: 'VabTabs',
|
||||
})
|
||||
|
||||
defineProps({
|
||||
layout: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { theme, device } = storeToRefs(settingsStore)
|
||||
const routesStore = useRoutesStore()
|
||||
const { getRoutes: routes } = storeToRefs(routesStore)
|
||||
const tabsStore = useTabsStore()
|
||||
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
|
||||
const _visitedRoutes = ref([...visitedRoutes.value])
|
||||
const {
|
||||
addVisitedRoute,
|
||||
delVisitedRoute,
|
||||
delOthersVisitedRoutes,
|
||||
delLeftVisitedRoutes,
|
||||
delRightVisitedRoutes,
|
||||
delAllVisitedRoutes,
|
||||
handleCaughtRoutes,
|
||||
updateVisitedRoutes,
|
||||
} = tabsStore
|
||||
const tabActive = ref('')
|
||||
const active = ref(false)
|
||||
const hoverRoute = ref()
|
||||
const visible = ref(false)
|
||||
const top = ref(0)
|
||||
const left = ref(0)
|
||||
const tabsSettingRef = ref(null)
|
||||
|
||||
const isActive = (path) => path === handleActivePath(route, true)
|
||||
const isNoClosable = (tag) => tag.meta && tag.meta.noClosable
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
if (!isActive(tab.name)) router.push(visitedRoutes.value[tab.index])
|
||||
}
|
||||
const handleVisibleChange = (value) => {
|
||||
active.value = value
|
||||
}
|
||||
|
||||
const initNoCLosableTabs = (routes) => {
|
||||
routes.forEach((_route) => {
|
||||
if (_route.meta && _route.meta.noClosable) addTabs(_route)
|
||||
if (_route.children) initNoCLosableTabs(_route.children)
|
||||
})
|
||||
}
|
||||
|
||||
const handleCommand = (command) => {
|
||||
switch (command) {
|
||||
case 'refresh': {
|
||||
refresh()
|
||||
break
|
||||
}
|
||||
case 'closeOthersTabs': {
|
||||
closeOthersTabs()
|
||||
break
|
||||
}
|
||||
case 'closeLeftTabs': {
|
||||
closeLeftTabs()
|
||||
break
|
||||
}
|
||||
case 'closeRightTabs': {
|
||||
closeRightTabs()
|
||||
break
|
||||
}
|
||||
case 'closeAllTabs': {
|
||||
closeAllTabs()
|
||||
break
|
||||
}
|
||||
case 'setting': {
|
||||
tabsSettingRef.value.handleOpenSetting()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前标签页
|
||||
*/
|
||||
const refresh = async () => {
|
||||
if (hoverRoute.value) {
|
||||
await router.push(hoverRoute.value)
|
||||
await $pub('reload-router-view', hoverRoute.value.name)
|
||||
} else await $pub('reload-router-view')
|
||||
await $pub('refresh-rotate')
|
||||
await closeMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加标签页
|
||||
* @param tag route
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const addTabs = async (tag) => {
|
||||
const tab = handleTabs(tag)
|
||||
if (tab) {
|
||||
await addVisitedRoute(tab)
|
||||
tabActive.value = tab.path
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据原生路径删除标签中的标签
|
||||
* @param rawPath 原生路径
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleTabRemove = async (rawPath) => {
|
||||
if (isActive(rawPath)) await toLastTab()
|
||||
await delVisitedRoute(rawPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除其他标签页
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const closeOthersTabs = async () => {
|
||||
if (hoverRoute.value) {
|
||||
await router.push(hoverRoute.value)
|
||||
await delOthersVisitedRoutes(hoverRoute.value.path)
|
||||
} else await delOthersVisitedRoutes(handleActivePath(route, true))
|
||||
await closeMenu()
|
||||
}
|
||||
/**
|
||||
* 删除左侧标签页
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const closeLeftTabs = async () => {
|
||||
if (hoverRoute.value) {
|
||||
await router.push(hoverRoute.value)
|
||||
await delLeftVisitedRoutes(hoverRoute.value.path)
|
||||
} else await delLeftVisitedRoutes(handleActivePath(route, true))
|
||||
await closeMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除右侧标签页
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const closeRightTabs = async () => {
|
||||
if (hoverRoute.value) {
|
||||
await router.push(hoverRoute.value)
|
||||
await delRightVisitedRoutes(hoverRoute.value.path)
|
||||
} else await delRightVisitedRoutes(handleActivePath(route, true))
|
||||
await closeMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除所有标签页
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const closeAllTabs = async () => {
|
||||
await delAllVisitedRoutes()
|
||||
await toLastTab()
|
||||
await closeMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转最后一个标签页
|
||||
*/
|
||||
const toLastTab = async () => {
|
||||
const latestView = visitedRoutes.value.findLast((item) => item.path !== handleActivePath(route, true))
|
||||
if (latestView) await router.push(latestView)
|
||||
else await router.push('/')
|
||||
}
|
||||
|
||||
const { x, y } = useMouse()
|
||||
|
||||
const openMenu = (item) => {
|
||||
left.value = x.value
|
||||
top.value = y.value
|
||||
hoverRoute.value = item
|
||||
hoverRoute.value.path = item.path
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const closeMenu = () => {
|
||||
visible.value = false
|
||||
hoverRoute.value = null
|
||||
}
|
||||
|
||||
let sortable
|
||||
const handleTabDrag = () => {
|
||||
if (theme.value.tabDrag && device.value != 'mobile') {
|
||||
const navElement = document.querySelector('.el-tabs__nav.is-top')
|
||||
if (navElement)
|
||||
sortable = new Sortable(navElement, {
|
||||
animation: 150,
|
||||
easing: 'cubic-bezier(1, 0, 0, 1)',
|
||||
draggable: '.el-tabs__item.is-top.is-closable',
|
||||
filter: '.el-tabs__active-bar.is-top',
|
||||
onEnd(e) {
|
||||
const routes = moveElement([...visitedRoutes.value], parseInt(e.oldIndex) - 1, parseInt(e.newIndex) - 1)
|
||||
updateVisitedRoutes(routes)
|
||||
_visitedRoutes.value = routes
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (visible.value) document.body.addEventListener('click', closeMenu)
|
||||
else document.body.removeEventListener('click', closeMenu)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
initNoCLosableTabs(routes.value)
|
||||
addTabs(route)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
theme.value,
|
||||
() => {
|
||||
if (theme.value.tabDrag) handleTabDrag()
|
||||
else sortable && sortable.destroy()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener('beforeunload', handleCaughtRoutes)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
handleTabDrag()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-tabs-more-dropdown {
|
||||
width: 135px;
|
||||
padding: calc(var(--el-padding) / 2) !important;
|
||||
|
||||
.el-dropdown-menu {
|
||||
padding: 0;
|
||||
|
||||
&__item {
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-popper-placement='bottom-end'] {
|
||||
.el-popper__arrow {
|
||||
left: 120px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-tabs {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: var(--el-tabs-height);
|
||||
padding-right: var(--el-padding);
|
||||
padding-left: var(--el-padding);
|
||||
user-select: none;
|
||||
background: var(--el-color-white);
|
||||
|
||||
.vab-tabs-draggable {
|
||||
width: calc(100% - var(--el-margin));
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.fold-unfold {
|
||||
margin-right: var(--el-margin);
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
&__nav-wrap::after {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-right: var(--el-padding) !important;
|
||||
padding-left: var(--el-padding) !important;
|
||||
|
||||
.is-icon-close {
|
||||
width: 14px !important;
|
||||
margin-top: 1px;
|
||||
margin-right: 0 !important;
|
||||
|
||||
svg {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__nav-next,
|
||||
&__nav-prev {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--el-tab-item-height);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: none;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
|
||||
&-card {
|
||||
height: var(--el-tab-item-height);
|
||||
|
||||
:deep() {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 1px 0;
|
||||
|
||||
.el-tabs__item {
|
||||
height: var(--el-tab-item-height);
|
||||
margin-right: 5px;
|
||||
border: 1px solid var(--el-border-color) !important;
|
||||
border-radius: var(--el-border-radius-base) !important;
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-smart {
|
||||
height: var(--el-tab-item-height);
|
||||
|
||||
:deep() {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 1px 0;
|
||||
|
||||
.el-tabs__item {
|
||||
height: var(--el-tab-item-height);
|
||||
margin-right: 5px;
|
||||
border: 0;
|
||||
border-top-left-radius: var(--el-border-radius-base);
|
||||
border-top-right-radius: var(--el-border-radius-base);
|
||||
|
||||
&.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
outline: none;
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
transition: var(--el-transition);
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
content: '';
|
||||
background-color: var(--el-color-primary);
|
||||
transition: var(--el-transition);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
transition: var(--el-transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-smooth {
|
||||
height: var(--el-tab-item-height);
|
||||
|
||||
:deep() {
|
||||
.el-tabs__nav {
|
||||
margin-top: 3.5px;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0 0 -7px 0;
|
||||
|
||||
.el-tabs__item {
|
||||
// min-width: 120px;
|
||||
height: calc(var(--el-tab-item-height) + 4px);
|
||||
margin-right: -18px;
|
||||
|
||||
&:hover {
|
||||
z-index: 999;
|
||||
color: var(--el-color-grey);
|
||||
background: var(--el-border-color);
|
||||
mask: url('/@/assets/tabs_images/vab-tab.png');
|
||||
mask-layer: url('/@/assets/tabs_images/vab-tab.png');
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.vab-tabs-title {
|
||||
flex: 1;
|
||||
margin: 0 calc(var(--el-margin) / 2) 0 calc(var(--el-margin) / 2);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
mask: url('/@/assets/tabs_images/vab-tab.png');
|
||||
mask-layer: url('/@/assets/tabs_images/vab-tab.png');
|
||||
mask-size: 100% 100%;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
mask: url('/@/assets/tabs_images/vab-tab.png');
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-rect {
|
||||
height: var(--el-tags-height);
|
||||
|
||||
:deep() {
|
||||
.el-tabs__header {
|
||||
margin: -1px 0 0 0;
|
||||
|
||||
.el-tabs__item {
|
||||
height: var(--el-tabs-height);
|
||||
|
||||
&.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-prev,
|
||||
.el-tabs__nav-next {
|
||||
height: var(--el-tabs-height);
|
||||
line-height: var(--el-tabs-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
padding: calc(var(--el-padding) / 2);
|
||||
|
||||
box-shadow: var(--el-box-shadow);
|
||||
|
||||
i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
&-more {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
text-align: left;
|
||||
|
||||
&-active,
|
||||
&:hover {
|
||||
&:after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.vab-tabs-more-icon {
|
||||
transform: rotate(90deg);
|
||||
|
||||
.box-t {
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.box:before,
|
||||
.box:after {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
display: inline-block;
|
||||
color: var(--el-color-grey);
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease-out;
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 8px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: '';
|
||||
background: var(--el-color-grey);
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: '';
|
||||
background: var(--el-color-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.box-t {
|
||||
&:before {
|
||||
transition: transform 0.3s ease-out 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user