初始化拉取,原文件名:admin-plus-webpack5
Some checks failed
Call HTTPS API / build (push) Has been cancelled

This commit is contained in:
2025-12-26 20:43:10 +08:00
parent 7c68ec3a42
commit 3e2da1115e
420 changed files with 75413 additions and 2 deletions

View File

@@ -0,0 +1,6 @@
module.exports = {
webpackBanner:
' build: Vue Admin' +
' Plus \n copyright: vue-admin-' +
'beautiful.com \n time: ',
}

View File

@@ -0,0 +1,12 @@
const Webpack = require('webpack')
const { webpackBanner } = require('./config.ts')
module.exports = {
createBanner: (config) => {
config
.plugin('banner')
.use(Webpack.BannerPlugin, [
`${webpackBanner}${process.env.VUE_APP_UPDATE_TIME}`,
])
},
}

View File

@@ -0,0 +1,22 @@
const dayjs = require('dayjs')
const { outputDir, abbreviation } = require('../../../../src/config')
const FileManagerPlugin = require('filemanager-webpack-plugin')
module.exports = {
createBuild7z: (config) => {
config.plugin('fileManager').use(FileManagerPlugin, [
{
events: {
onEnd: {
archive: [
{
source: `./${outputDir}`,
destination: `./${outputDir}/${abbreviation}_${dayjs().unix()}.zip`,
},
],
},
},
},
])
},
}

View File

@@ -0,0 +1,16 @@
const productionGzipExtensions = ['html', 'js', 'css', 'svg']
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
createGzip: (config) => {
config.plugin('compression').use(CompressionWebpackPlugin, [
{
filename: '[path][base].gz[query]',
algorithm: 'gzip',
test: new RegExp(`\\.(${productionGzipExtensions.join('|')})$`),
threshold: 8192,
minRatio: 0.8,
},
])
},
}

View File

@@ -0,0 +1,12 @@
module.exports = {
createImageCompression: (config) => {
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
bypassOnDebug: true,
})
.end()
},
}

View File

@@ -0,0 +1,43 @@
const { createGzip } = require('./gzip/index.ts')
const { createBanner } = require('./banner/index.ts')
const { createBuild7z } = require('./build7z/index.ts')
const { createSvgSprite } = require('./svgSprite/index.ts')
const { createOptimization } = require('./optimization/index.ts')
const { createSourceInjector } = require('./sourceInjector/index.ts')
const { createImageCompression } = require('./imageCompression/index.ts')
const { build7z, buildGzip, imageCompression } = require('../../../src/config')
const path = require('path')
module.exports = {
createChainWebpack: (env, config) => {
config.resolve.symlinks(true)
createBanner(config)
createSvgSprite(config)
if (env === 'production') {
if (build7z) createBuild7z(config)
if (buildGzip) createGzip(config)
if (imageCompression && process.env.VAB_VARIABLE !== 'website')
createImageCompression(config)
createOptimization(config)
}
if (env === 'development') config.devtool('cheap-module-source-map')
createSourceInjector(config)
// 添加一些构建优化
// 避免处理node_modules中已经编译过的文件
config.module
.rule('js')
.include.add(path.resolve('src'))
.add(path.resolve('library'))
.end()
.exclude.add(/node_modules/)
.end()
// 优化构建性能
config.plugin('fork-ts-checker').tap((options) => {
options[0].formatter = 'codeframe'
options[0].async = false
return options
})
},
}

View File

@@ -0,0 +1,74 @@
const rely = require('call-' + 'rely')
const { resolve } = require('path')
module.exports = {
createOptimization: (config) => {
process.env['VUE_AP' + 'P_RELY'] = rely
config.performance.set('hints', false)
config.optimization.splitChunks({
automaticNameDelimiter: '-',
chunks: 'all',
cacheGroups: {
// 默认缓存组
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
// 公共chunk
common: {
name: 'vab-common',
minChunks: 2,
priority: -10,
chunks: 'initial',
maxInitialRequests: 5,
minSize: 0,
},
chunk: {
name: 'vab-chunk',
test: /[\\/]node_modules[\\/]/,
minSize: 131072,
maxSize: 524288,
chunks: 'initial',
minChunks: 2,
priority: 10,
},
vue: {
name: 'vue',
test: /[\\/]node_modules[\\/](vue(.*)|core-js)[\\/]/,
chunks: 'initial',
priority: 20,
},
elementPlus: {
name: 'element-plus',
test: /[\\/]node_modules[\\/]_?element-plus(.*)/,
priority: 30,
chunks: 'all',
},
extra: {
name: 'vab-plugins',
test: resolve('src/plugins'),
priority: 40,
},
components: {
name: 'vab-components',
test: resolve('library/components'),
priority: 50,
},
xlsx: {
name: 'xlsx',
test: /[\\/]node_modules[\\/]_?xlsx(.*)/,
priority: 60,
},
echarts: {
name: 'echarts',
test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/,
priority: 65,
chunks: 'all',
},
},
})
// 配置runtimeChunk
config.optimization.runtimeChunk('single')
},
}

View File

@@ -0,0 +1,11 @@
const injector = require.resolve('./injector.ts')
module.exports = {
createSourceInjector: (config) => {
config.module
.rule('vue')
.use('vue-filename-injector')
.loader(injector)
.after('vue-loader')
},
}

View File

@@ -0,0 +1,14 @@
const { relative } = require('path')
const blockName = 'vue-filename-injector'
module.exports = function (content) {
const { rootContext, resourcePath } = this
const context = rootContext || process.cwd()
const filePath = relative(context, resourcePath).replace(/\\/g, '/')
content += `<${blockName}>
export default function (Component) {
Component.__source = ${JSON.stringify(filePath)}
}
</${blockName}>`
return content
}

View File

@@ -0,0 +1,15 @@
const { resolve } = require('path')
module.exports = {
createSvgSprite: (config) => {
config.module.rule('svg').exclude.add(resolve('src/icon'))
config.module
.rule('vabIcon')
.test(/\.svg$/)
.include.add(resolve('src/icon'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({ symbolId: 'vab-icon-[name]' })
},
}

7
library/build/index.ts Normal file
View File

@@ -0,0 +1,7 @@
const { createVuePlugin } = require('./vuePlugins/index.ts')
const { createChainWebpack } = require('./chainWebpack/index.ts')
module.exports = {
createVuePlugin,
createChainWebpack,
}

View File

@@ -0,0 +1,322 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElLoading: typeof import('element-plus/es')['ElLoading']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElNotification: typeof import('element-plus/es')['ElNotification']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const axios: typeof import('axios')['default']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const manualResetRef: typeof import('@vueuse/core')['manualResetRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refManualReset: typeof import('@vueuse/core')['refManualReset']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

123
library/build/vuePlugins/components.d.ts vendored Normal file
View File

@@ -0,0 +1,123 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElBadge: typeof import('element-plus/es')['ElBadge']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRate: typeof import('element-plus/es')['ElRate']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VabApp: typeof import('./../../components/VabApp/index.vue')['default']
VabAppMain: typeof import('./../../components/VabAppMain/index.vue')['default']
VabAvatar: typeof import('./../../components/VabAvatar/index.vue')['default']
VabBreadcrumb: typeof import('./../../components/VabBreadcrumb/index.vue')['default']
VabCard: typeof import('./../../components/VabCard/index.vue')['default']
VabColorfulCard: typeof import('./../../components/VabColorfulCard/index.vue')['default']
VabColumnBar: typeof import('./../../components/VabColumnBar/index.vue')['default']
VabErrorLog: typeof import('./../../components/VabErrorLog/index.vue')['default']
VabFold: typeof import('./../../components/VabFold/index.vue')['default']
VabFooter: typeof import('./../../components/VabFooter/index.vue')['default']
VabFullScreen: typeof import('./../../components/VabFullScreen/index.vue')['default']
VabHeader: typeof import('./../../components/VabHeader/index.vue')['default']
VabLanguage: typeof import('./../../components/VabLanguage/index.vue')['default']
VabLink: typeof import('./../../components/VabLink/index.vue')['default']
VabLock: typeof import('./../../components/VabLock/index.vue')['default']
VabLogo: typeof import('./../../components/VabLogo/index.vue')['default']
VabMenu: typeof import('./../../components/VabMenu/index.vue')['default']
VabMenuItem: typeof import('./../../components/VabMenu/components/VabMenuItem.vue')['default']
VabNav: typeof import('./../../components/VabNav/index.vue')['default']
VabNotice: typeof import('./../../components/VabNotice/index.vue')['default']
VabQueryForm: typeof import('./../../components/VabQueryForm/index.vue')['default']
VabQueryFormBottomPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormBottomPanel.vue')['default']
VabQueryFormLeftPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormLeftPanel.vue')['default']
VabQueryFormRightPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormRightPanel.vue')['default']
VabQueryFormTopPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormTopPanel.vue')['default']
VabRefresh: typeof import('./../../components/VabRefresh/index.vue')['default']
VabRouterView: typeof import('./../../components/VabRouterView/index.vue')['default']
VabSearch: typeof import('./../../components/VabSearch/index.vue')['default']
VabSideBar: typeof import('./../../components/VabSideBar/index.vue')['default']
VabSubMenu: typeof import('./../../components/VabMenu/components/VabSubMenu.vue')['default']
VabTabs: typeof import('./../../components/VabTabs/index.vue')['default']
VabTheme: typeof import('./../../components/VabTheme/index.vue')['default']
VabThemeDrawer: typeof import('./../../components/VabTheme/components/VabThemeDrawer.vue')['default']
VabThemeSetting: typeof import('./../../components/VabTheme/components/VabThemeSetting.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
createDefineOptions: () => [
require('unplugin-vue-define-options/webpack')(),
],
}

View File

@@ -0,0 +1,10 @@
// @ts-ignore
const Webpack = require('webpack')
module.exports = {
createDefinePlugin: () => [
new Webpack.DefinePlugin({
__APP_INFO__: process.env.VUE_APP_INFO,
}),
],
}

View File

@@ -0,0 +1,19 @@
const { createUnPlugin } = require('vue-' + 'unplugins')
const { createWebpackBar } = require('./webpack' + 'Bar/index.ts')
const { createDefineOptions } = require('./defineOptions/index.ts')
const { createDefinePlugin } = require('./definePlugin/index.ts')
const { createProvidePlugin } = require('./providePlugin/index.ts')
const { createMinChunkSizePlugin } = require('./minChunkSizePlugin/index.ts')
const dev = process.env.NODE_ENV === 'development'
module.exports = {
createVuePlugin: () => [
...createDefineOptions(),
...createUnPlugin(),
require('unplugin-element-plus/webpack')(),
...createWebpackBar(),
...createDefinePlugin(),
...createProvidePlugin(),
...(dev ? [] : createMinChunkSizePlugin()),
],
}

View File

@@ -0,0 +1,14 @@
// @ts-ignore
const Webpack = require('webpack')
const { buildOptimize } = require('../../../../src/config')
module.exports = {
createMinChunkSizePlugin: () =>
buildOptimize
? []
: [
new Webpack.optimize.MinChunkSizePlugin({
minChunkSize: 1024 * 300,
}),
],
}

View File

@@ -0,0 +1,7 @@
// @ts-ignore
const Webpack = require('webpack')
const { providePlugin } = require('../../../../src/config')
module.exports = {
createProvidePlugin: () => [new Webpack.ProvidePlugin(providePlugin)],
}

View File

@@ -0,0 +1,10 @@
const WebpackBar = require('webpackbar')
const { version } = require('../../../../package.json')
module.exports = {
createWebpackBar: () => [
new WebpackBar({
name: `Vue-` + `Admin` + `-Plus ${version}`,
}),
],
}

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { useHead } from '@vueuse/head'
import { pwa } from '@/config'
import { enLocale, zhLocale } from '@/i18n'
const route = useRoute()
const { locale: language } = useI18n()
const locale = computed(() =>
language.value === 'en' ? enLocale : zhLocale
)
const VabUpdate = defineAsyncComponent(
() => import('@/plugins/VabUpdate/index.vue')
)
const siteData = reactive({
description: '',
})
watchEffect(() => {
siteData.description = `${'Vue'} ${'Admin'} ${'Plus'}-${route.meta.title} - 简介、官网、首页、文档和下载 - 前端开发框架`
})
useHead({
meta: [
{
name: `description`,
content: computed(() => siteData.description),
},
],
})
</script>
<template>
<el-config-provider
:button="{
autoInsertSpace: true,
}"
:locale="locale"
>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
<vab-update v-if="pwa" ref="vabUpdateRef" />
</el-config-provider>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { handleActivePath } from '@/utils/routes'
const route = useRoute()
const routesStore: any = 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>
<template>
<div class="vab-app-main">
<section>
<vab-router-view />
</section>
<vab-footer />
</div>
</template>

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { toLoginRoute } from '@/utils/routes'
import { translate } from '@/i18n'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { avatar, username } = storeToRefs(userStore)
const { logout } = userStore
const active = ref(false)
const handleVisibleChange = (val: boolean) => {
active.value = val
}
const handleCommand = async (command: string) => {
switch (command) {
case 'logout':
await logout()
await router.push(toLoginRoute(route.fullPath))
break
case 'personalCenter':
await router.push('/setting/personalCenter')
break
case 'friendlyTip':
await router.push('/friendly-tip')
break
case 'shop':
await window.open('https://vuejs-core.cn/shop-vite')
break
}
}
</script>
<template>
<el-dropdown @command="handleCommand" @visible-change="handleVisibleChange">
<span class="avatar-dropdown">
<el-avatar class="user-avatar" :src="avatar" />
<div class="user-name">
<span class="hidden-xs-only" :title="username">
{{ username }}
</span>
<vab-icon
class="vab-dropdown"
:class="{ 'vab-dropdown-active': active }"
icon="arrow-down-s-line"
/>
</div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="personalCenter">
<vab-icon icon="user-line" />
<span>{{ translate('个人中心') }}</span>
</el-dropdown-item>
<el-dropdown-item command="shop">
<vab-icon icon="vuejs-line" />
<span>{{ translate('shop vite') }}</span>
</el-dropdown-item>
<el-dropdown-item command="logout">
<vab-icon icon="logout-circle-r-line" />
<span>{{ translate('退出登录') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style lang="scss" scoped>
.avatar-dropdown {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
justify-items: center;
.user-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
margin-left: 15px;
cursor: pointer;
border-radius: 50%;
}
.user-name {
position: relative;
display: flex;
flex-shrink: 0;
align-content: center;
align-items: center;
height: 40px;
margin-left: 6px;
line-height: 40px;
cursor: pointer;
span {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[class*='ri-'] {
margin-left: 3px !important;
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { translate } from '@/i18n'
import { handleMatched } from '@/utils/routes'
const route = useRoute()
const routesStore = useRoutesStore()
const { getRoutes: routes } = storeToRefs(routesStore)
const breadcrumbList = computed(() =>
handleMatched(routes.value, route.path).filter(
(item: any) => !item.meta.breadcrumbHidden
)
)
const handleTo = (path: string | undefined = '') => {
return { path }
}
</script>
<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.icon"
:icon="item.meta.icon"
:is-custom-svg="item.meta.isCustomSvg"
/>
<span v-if="item.meta.title">{{ translate(item.meta.title) }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<style lang="scss" scoped>
.vab-breadcrumb {
height: $base-nav-height;
font-size: $base-font-size-default;
line-height: $base-nav-height;
:deep() {
.el-breadcrumb__item {
.el-breadcrumb__inner {
font-weight: normal;
color: #515a6e;
i,
svg {
margin-right: 3px;
}
}
&:last-child {
.el-breadcrumb__inner {
a {
color: #999;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
defineProps({
header: {
type: String,
default: '',
},
bodyStyle: {
type: Object,
default: () => {
return {}
},
},
shadow: {
type: String,
default: 'never',
} as any,
skeleton: {
type: Boolean,
default: false,
},
skeletonRows: {
type: Number,
default: 4, //显示的数量会比传入的数量多 1
},
})
let timer: any = null
const skeletonShow = ref(true)
timer = setTimeout(() => {
skeletonShow.value = false
}, 500)
onBeforeRouteLeave((to, from, next) => {
clearInterval(timer)
next()
})
</script>
<template>
<el-card :body-style="bodyStyle" class="vab-card" :shadow="shadow">
<template v-if="$slots.header || header" #header>
<slot name="header">{{ header }}</slot>
</template>
<el-skeleton
v-if="skeleton"
animated
:loading="skeletonShow"
:rows="skeletonRows"
>
<template #default>
<slot class="vab-card-transition" />
</template>
</el-skeleton>
<slot v-else class="vab-card-transition" />
</el-card>
</template>
<style lang="scss" scoped>
.vab-card {
&-transition {
transition: $base-transition;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
defineProps({
shadow: {
type: String,
default: 'never',
} as any,
colorFrom: {
type: String,
default: '',
},
colorTo: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
})
</script>
<template>
<el-card
class="vab-colorful-card"
:shadow="shadow"
:style="{
background: `linear-gradient(50deg, ${colorFrom}, ${colorTo})`,
}"
>
<template #header>{{ title }}</template>
<vab-icon v-if="icon" :icon="icon" />
<slot />
</el-card>
</template>
<style lang="scss" scoped>
.vab-colorful-card {
position: relative;
min-height: 120px;
cursor: pointer;
* {
color: var(--el-color-white);
}
:deep() {
.el-card__header {
color: var(--el-color-white);
border-bottom: 0;
}
.el-card__body {
padding-top: 0;
}
}
i {
position: absolute;
top: -30px;
right: 20px;
font-size: 60px;
transform: rotate(15deg);
}
}
</style>

View File

@@ -0,0 +1,392 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { isExternal } from '@/utils/validate'
import { translate } from '@/i18n'
import { useRoutesStore } from '@/store/modules/routes'
import { defaultOpeneds, openFirstMenu, uniqueOpened } from '@/config'
import { useSettingsStore } from '@/store/modules/settings'
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const { theme, collapse } = storeToRefs(settingsStore)
const { foldSideBar, openSideBar } = settingsStore
const routesStore = useRoutesStore()
const {
getTab: tab,
getTabMenu: tabMenu,
getActiveMenu: activeMenu,
getRoutes: routes,
getPartialRoutes: partialRoutes,
}: any = storeToRefs(routesStore)
const handleTabClick = () => {
nextTick(() => {
if (isExternal(tabMenu.value.path)) {
window.open(tabMenu.value.path)
setTimeout(() => {
router.push(`/direct?path=/`)
}, 500)
} else if (openFirstMenu)
router.push(tabMenu.value.redirect || tabMenu.value)
})
}
watchEffect(() => {
const foldUnfold: any = document.querySelector(
'.fold-unfold'
) as HTMLElement
if (theme.value.layout === 'column' && route.meta.noColumn) {
foldSideBar()
if (foldUnfold) foldUnfold.style = 'display:none'
} else {
openSideBar()
if (foldUnfold) foldUnfold.style = ''
}
})
</script>
<template>
<el-scrollbar
class="vab-column-bar-container"
:class="{
'is-collapse': collapse,
['vab-column-bar-container-' + theme.columnStyle]: true,
}"
>
<vab-logo />
<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>
{{ translate(item.meta.title) }}
</span>
</div>
</div>
</template>
</el-tab-pane>
</template>
</el-tabs>
<el-menu
:background-color="variables['column-second-menu-background']"
:default-active="activeMenu.data"
:default-openeds="defaultOpeneds"
mode="vertical"
:unique-opened="uniqueOpened"
>
<el-divider>
{{ translate(tabMenu ? tabMenu.meta.title : tabMenu) }}
</el-divider>
<template v-for="item in partialRoutes" :key="item.path">
<vab-menu v-if="!item.meta.hidden" :item="item" />
</template>
</el-menu>
</el-scrollbar>
</template>
<style lang="scss" scoped>
@use 'sass:math';
@mixin active {
&:hover {
color: var(--el-color-primary);
background-color: $base-column-second-menu-active !important;
i,
svg {
color: var(--el-color-primary);
}
}
&.is-active {
color: var(--el-color-primary);
background-color: $base-column-second-menu-active !important;
}
}
.vab-column-bar-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: var(--el-left-menu-width);
height: 100vh;
overflow: hidden;
background: $base-column-second-menu-background;
box-shadow: $base-box-shadow;
&-vertical,
&-card,
&-arrow {
:deep() {
.el-tabs + .el-menu {
left: $base-left-menu-width-min;
width: calc(
var(--el-left-menu-width) - #{$base-left-menu-width-min}
);
border: 0;
}
}
}
&-horizontal {
:deep() {
.logo-container-column {
.logo {
width: $base-left-menu-width-min * 1.3 !important;
}
.title {
margin-left: $base-left-menu-width-min * 1.3 !important;
}
}
.el-tabs + .el-menu {
left: $base-left-menu-width-min * 1.3;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} * 1.3
);
border: 0;
}
}
}
&-card {
:deep() {
.el-tabs {
.el-tabs__item {
padding: 5px !important;
.vab-column-grid {
width: $base-left-menu-width-min - 10 !important;
height: $base-left-menu-width-min - 10 !important;
border-radius: 5px;
}
&.is-active {
background: transparent !important;
.vab-column-grid {
background: var(--el-color-primary);
}
}
}
}
.el-tabs + .el-menu {
left: $base-left-menu-width-min + 10;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} - 20px
);
}
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
min-width: 180px;
margin-bottom: 5px;
border-radius: 5px;
}
}
}
&-arrow {
:deep() {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
.vab-column-grid {
background: transparent !important;
&:after {
position: absolute;
right: 0;
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: $base-left-menu-width-min + 10;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} - 20px
);
}
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
min-width: 180px;
margin-bottom: 5px;
border-radius: 5px;
}
}
}
.vab-column-grid {
display: flex;
align-items: center;
width: $base-left-menu-width-min;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
word-break: break-all;
white-space: nowrap;
&-vertical,
&-card,
&-arrow {
justify-content: center;
height: $base-left-menu-width-min;
> div {
svg {
position: relative;
display: block;
width: $base-font-size-default + 2;
height: $base-font-size-default + 2;
margin: auto !important;
}
[class*='ri-'] {
display: block;
height: 20px;
}
}
}
&-horizontal {
justify-content: left;
width: $base-left-menu-width-min * 1.3;
height: #{math.div($base-left-menu-width-min, 1.3)};
padding-left: #{math.div($base-padding, 2)};
}
}
:deep() {
* {
transition: $base-transition;
}
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-tabs {
position: fixed;
box-shadow: $base-box-shadow;
.el-tabs__header.is-left {
margin-right: 0 !important;
.el-tabs__nav-wrap.is-left {
margin-right: 0 !important;
background: $base-column-first-menu-background;
.el-tabs__nav-scroll {
height: calc(
100vh - #{$base-logo-height}
) !important;
overflow-y: auto !important;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.el-tabs__nav {
height: auto !important;
overflow: hidden !important;
background: $base-column-first-menu-background;
}
}
}
.el-tabs__item {
height: auto;
padding: 0;
color: var(--el-color-white);
&.is-active {
background: var(--el-color-primary);
}
}
}
.el-tabs__nav-prev,
.el-tabs__active-bar.is-left,
.el-tabs--left .el-tabs__nav-wrap.is-left::after {
display: none;
}
.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable {
padding: 0 !important;
}
.el-menu {
border: 0;
.el-divider {
margin: 0 0 #{$base-margin} 0;
background-color: #f6f6f6;
&__text {
color: var(--el-color-black);
}
}
.el-menu-item,
.el-sub-menu__title {
height: $base-menu-item-height;
overflow: hidden;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include active;
}
}
}
&.is-collapse {
:deep() {
width: 0;
}
}
}
</style>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import { useErrorLogStore } from '@/store/modules/errorLog'
const errorLogStore = useErrorLogStore()
const { errorLogs } = storeToRefs(errorLogStore)
const { clearErrorLog } = errorLogStore
const state = reactive({
dialogVisible: false,
})
const searchList = [
{
title: '百度搜索',
url: 'https://www.baidu.com/baidu?wd=',
icon: 'baidu-line',
},
{
title: '谷歌搜索',
url: 'https://www.google.com/search?q=',
icon: 'google-line',
},
]
const clearAll = () => {
state.dialogVisible = false
clearErrorLog()
}
</script>
<template>
<div v-if="errorLogs.length > 0">
<el-badge
type="danger"
:value="errorLogs.length"
@click="state.dialogVisible = true"
>
<vab-icon icon="bug-line" />
</el-badge>
<el-dialog
v-model="state.dialogVisible"
append-to-body
title="admin-plus 异常捕获(温馨提示:错误必须解决)"
width="70%"
>
<el-table border :data="errorLogs">
<el-table-column label="报错路由">
<template #default="{ row }">
<a :href="row.url" target="_blank">
<el-tag type="success">{{ row.url }}</el-tag>
</a>
</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="错误详情" width="120">
<template #default="{ row }">
<el-popover placement="top-start" trigger="hover">
{{ row.err.stack }}
<template #reference>
<el-button>查看</el-button>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template #default="{ row }">
<a
v-for="(item, index) in searchList"
:key="index"
:href="item.url + row.err.message"
target="_blank"
>
<el-button>
<vab-icon :icon="item.icon" />
{{ item.title }}
</el-button>
</a>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="state.dialogVisible = false">
</el-button>
<el-button type="danger" @click="clearAll">暂不显示</el-button>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-badge) {
.el-button {
display: flex;
align-items: center;
justify-items: center;
height: 28px;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const { toggleCollapse } = settingsStore
</script>
<template>
<vab-icon
class="fold-unfold"
:icon="collapse ? 'menu-unfold-line' : 'menu-fold-line'"
@click="toggleCollapse"
/>
</template>
<style lang="scss" scoped>
.fold-unfold {
color: var(--el-color-grey);
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const fullYear = new Date().getFullYear()
const settingsStore = useSettingsStore()
const { title } = storeToRefs(settingsStore)
// 国家法律法规要求显示备案号 实际项目请自行修改为自己的备案信息及域名
// const beianShow = ref(false)
</script>
<template>
<footer class="vab-footer">
Copyright
<vab-icon icon="copyright-line" />
{{ fullYear }} {{ title }}
<!-- <a
v-if="beianShow"
class="beian"
href="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
>
鲁ICP备2021002317号-1
</a> -->
</footer>
</template>
<style lang="scss" scoped>
.vab-footer {
display: flex;
align-items: center;
justify-content: center;
min-height: 55px;
padding: 0 #{$base-padding} 0 #{$base-padding};
color: rgba(0, 0, 0, 0.45);
background: var(--el-color-white);
border-top: 1px dashed #{$base-border-color};
i {
margin: 0 5px;
}
.beian {
margin-left: 5px;
user-select: none;
}
}
</style>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const { isFullscreen, toggle } = useFullscreen()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
</script>
<template>
<vab-icon
v-if="theme.showFullScreen"
:icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'"
@click="toggle"
/>
</template>

View File

@@ -0,0 +1,233 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { useRoutesStore } from '@/store/modules/routes'
defineProps({
layout: {
type: String,
default: 'horizontal',
},
})
const routesStore = useRoutesStore()
const { getActiveMenu: activeMenu, getRoutes: routes } =
storeToRefs(routesStore)
</script>
<template>
<div class="vab-header">
<div class="vab-main">
<el-row :gutter="20">
<el-col :span="6">
<vab-logo />
</el-col>
<el-col :span="18">
<div class="right-panel">
<el-menu
v-if="'horizontal' === layout"
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:default-active="activeMenu.data"
menu-trigger="hover"
mode="horizontal"
style="width: 100%"
:text-color="variables['menu-color']"
>
<template
v-for="(item, index) in routes.flatMap(
(route) =>
route['meta'] &&
route['meta']['levelHidden'] &&
route['children']
? [...route['children']]
: route
)"
>
<vab-menu
v-if="
item['meta'] && !item['meta']['hidden']
"
:key="index + item['name']"
:item="item"
:layout="layout"
/>
</template>
</el-menu>
<vab-error-log />
<vab-lock />
<vab-search />
<vab-notice />
<vab-full-screen />
<vab-language />
<vab-theme />
<vab-refresh />
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<style lang="scss" scoped>
@use 'sass:math';
$base-menu-height: 40px;
.vab-header {
display: flex;
align-items: center;
justify-items: flex-end;
height: $base-header-height;
background: $base-menu-background;
.vab-main {
padding: 0 #{$base-padding} 0 #{$base-padding};
.right-panel {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-header-height;
:deep() {
.el-sub-menu__icon-more {
margin-top: #{math.div($base-menu-height - 20, 2)} !important;
margin-right: 20px !important;
}
> .el-menu--horizontal.el-menu {
> .el-sub-menu > .el-sub-menu__title {
padding-right: 0;
> .el-sub-menu__icon-arrow {
position: relative !important;
margin-top: -5px !important;
margin-right: 0;
margin-left: 30px;
}
}
> .el-menu-item {
.el-tag {
position: relative !important;
margin-top: 0 !important;
margin-right: -20px;
margin-left: 25px;
}
.vab-dot {
float: right;
margin-top: #{math.div(
$base-header-height - 6,
2
)} +
1;
}
@media only screen and (max-width: 1199px) {
.el-tag {
display: none;
}
}
}
}
.el-menu {
border: 0 !important;
* {
border: 0 !important;
}
&.el-menu--horizontal {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: $base-menu-height;
border: 0 !important;
> .el-menu-item,
> .el-sub-menu {
height: $base-menu-height;
margin-right: 3px;
line-height: $base-menu-height;
border-radius: 3px;
.el-sub-menu__icon-arrow {
float: right;
margin-top: -6px;
}
> .el-sub-menu__title {
display: flex;
align-items: flex-start;
height: $base-menu-height;
line-height: $base-menu-height;
border: 0 !important;
border-radius: 3px;
}
}
}
[class*='ri-'],
.vab-icon {
margin-left: 0;
color: var(--el-color-white);
cursor: pointer;
}
.el-sub-menu {
.vab-icon {
margin-top: 12px !important;
}
}
.el-sub-menu,
.el-menu-item {
i,
.vab-icon {
color: inherit;
}
&.is-active {
border: 0 !important;
.el-sub-menu__title {
border: 0 !important;
}
}
}
.el-menu-item {
&.is-active {
background: var(--el-color-primary) !important;
}
}
}
.user-name {
color: var(--el-color-white);
}
.user-name + i {
color: var(--el-color-white);
}
[class*='ri-'] {
margin-left: $base-margin;
color: var(--el-color-white);
cursor: pointer;
}
button {
svg {
margin-right: 0;
color: var(--el-color-white);
cursor: pointer;
fill: var(--el-color-white);
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import getPageTitle from '@/utils/pageTitle'
const { locale } = useI18n()
const route = useRoute()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const { changeLanguage } = settingsStore
const handleCommand = (language: string) => {
changeLanguage(language)
locale.value = language
document.title = getPageTitle(route.meta.title)
}
</script>
<template>
<el-dropdown v-if="theme.showLanguage" @command="handleCommand">
<vab-icon icon="translate" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh">中文简体</el-dropdown-item>
<el-dropdown-item command="en">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { isExternal } from '@/utils/validate'
const props = defineProps({
to: {
type: String,
required: true,
},
})
const type = computed(() => (isExternal(props.to) ? 'a' : 'router-link'))
const linkProps = () =>
isExternal(props.to)
? {
href: props.to,
target: '_blank',
rel: 'noopener',
}
: { to: props.to }
</script>
<template>
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>

View File

@@ -0,0 +1,225 @@
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
const vFocus: any = {
mounted(el: HTMLElement) {
el.querySelector('input')?.focus()
},
}
const userStore = useUserStore()
const { avatar } = storeToRefs(userStore)
const settingsStore = useSettingsStore()
const { theme, lock, title } = storeToRefs(settingsStore)
const { handleLock: _handleLock, handleUnLock: _handleUnLock } =
settingsStore
const background = ref(
`https://gcore.jsdelivr.net/gh/zxwk1998/image/vab-image-lock/${Math.round(Math.random() * 31)}.jpg`
)
const randomBackground = () => {
background.value = `https://gcore.jsdelivr.net/gh/zxwk1998/image/vab-image-lock/${Math.round(Math.random() * 31)}.jpg`
}
const validatePass = (rule: any, value: string, callback: any) => {
if (value === '' || value !== '123456') {
callback(new Error('请输入正确的密码'))
} else {
callback()
}
}
const formRef = ref()
const form = ref({
password: '123456',
})
const rules = {
password: [{ validator: validatePass, trigger: 'blur' }],
}
let lockIcon = true
const handleUnLock = () => {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
lockIcon = false
setTimeout(async () => {
await _handleUnLock()
lockIcon = true
await randomBackground()
const el = document.querySelector(
'.vab-side-bar'
) as HTMLElement
if (el) el.removeAttribute('style')
}, 500)
}
})
}
const handleLock = () => {
_handleLock()
const el = document.querySelector('.vab-side-bar') as HTMLElement
if (el) el.style.display = 'none'
}
</script>
<template>
<vab-icon v-if="theme.showLock" icon="lock-line" @click="handleLock" />
<transition v-if="theme.showLock" mode="out-in" name="fade-transform">
<div v-if="lock" class="vab-screen-lock">
<div
class="vab-screen-lock-background"
:style="{
background: `fixed url(${background}) center`,
backgroundSize: '100% 100%',
filter: 'blur(10px)',
}"
></div>
<div class="vab-screen-lock-content">
<div class="vab-screen-lock-content-title">
<el-avatar :size="180" :src="avatar" />
<vab-icon
:icon="lockIcon ? 'lock-line' : 'lock-unlock-line'"
/>
{{ title }} {{ translate('屏幕已锁定') }}
</div>
<div class="vab-screen-lock-content-form">
<el-form
ref="formRef"
:model="form"
:rules="rules"
@submit.prevent
>
<el-form-item label="" :label-width="0" prop="password">
<el-input
v-model="form.password"
v-focus
autocomplete="off"
placeholder="请输出密码123456"
type="password"
>
<template #suffix>
<el-button
native-type="submit"
type="primary"
@click="handleUnLock"
>
<vab-icon
:icon="
lockIcon
? 'lock-line'
: 'lock-unlock-line'
"
/>
{{ translate('解锁') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</div>
<span @click="randomBackground">
{{ translate('切换壁纸') }}
</span>
</div>
</div>
</transition>
</template>
<style lang="scss" scoped>
.vab-screen-lock {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
font-weight: bold;
background-color: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
transition: $base-transition;
&-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index - 1;
}
&-content {
z-index: $base-z-index;
padding: 40px 95px 40px 95px;
color: #252a30;
text-align: center;
background: rgba(255, 255, 255, 0.6);
border-radius: 15px;
backdrop-filter: blur(10px);
> span {
font-size: $base-font-size-small;
cursor: pointer;
}
&-title {
line-height: 50px;
color: #252a30;
text-align: center;
.ri-lock-line,
.ri-lock-unlock-line {
display: block;
margin: auto !important;
font-size: 30px;
color: #252a30 !important;
transition: $base-transition;
}
}
&-form {
:deep() {
.el-input__inner {
width: 280px;
height: 40px;
line-height: 40px;
}
.el-input__wrapper {
padding-right: 0;
.el-input__suffix {
right: 0;
.el-button {
height: 40px;
line-height: 40px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
i {
margin-left: 0 !important;
}
}
.el-input__validateIcon {
display: none;
}
}
}
}
}
}
@media (max-width: 576px) {
.vab-screen-lock-content {
width: auto !important;
margin: 5vw;
}
}
}
</style>

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const { theme, logo, title } = storeToRefs(settingsStore)
</script>
<template>
<div
class="logo-container"
:class="{
['logo-container-' + theme.layout]: true,
}"
>
<router-link to="/">
<span class="logo">
<!-- 使用自定义svg示例 -->
<vab-icon v-if="logo" :icon="logo" is-custom-svg />
</span>
<span
class="title"
:class="{ 'hidden-xs-only': theme.layout === 'horizontal' }"
>
{{ title }}
</span>
</router-link>
</div>
</template>
<style lang="scss" scoped>
@mixin container {
position: relative;
height: $base-header-height;
overflow: hidden;
line-height: $base-header-height;
background: transparent;
}
@mixin logo {
display: inline-block;
width: 32px;
height: 32px;
vertical-align: middle;
color: $base-title-color;
fill: currentColor;
}
@mixin title {
display: inline-block;
margin-left: 5px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
line-height: 55px;
vertical-align: middle;
color: $base-title-color;
white-space: nowrap;
}
.logo-container {
&-horizontal,
&-common {
@include container;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
}
}
&-vertical,
&-column,
&-comprehensive,
&-float {
@include container;
height: $base-logo-height;
line-height: $base-logo-height;
text-align: center;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
max-width: calc(var(--el-left-menu-width) - 60px);
}
}
&-column {
background: $base-column-second-menu-background !important;
.logo {
position: fixed;
top: 0;
display: block;
width: $base-left-menu-width-min;
height: $base-logo-height;
margin: 0;
background: $base-column-first-menu-background;
}
.title {
padding-right: 15px;
padding-left: 15px;
margin-left: $base-left-menu-width-min !important;
color: var(--el-color-black) !important;
background: $base-column-second-menu-background !important;
@include title;
}
}
}
</style>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { isExternal } from '@/utils/validate'
import { translate } from '@/i18n'
import { isHashRouterMode } from '@/config'
const props = defineProps({
itemOrMenu: {
type: Object,
default() {
return null
},
},
})
const route = useRoute()
const router = useRouter()
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { device } = storeToRefs(settingsStore)
const { foldSideBar } = settingsStore
const handleLink = () => {
const routePath = props.itemOrMenu.path
const target = props.itemOrMenu.meta.target
if (target === '_blank') {
if (isExternal(routePath)) window.open(routePath)
else if (route.path !== routePath)
isHashRouterMode
? window.open(`/#${routePath}`)
: window.open(routePath)
} else {
if (isExternal(routePath)) window.location.href = routePath
else if (route.path !== routePath) {
if (device.value === 'mobile') foldSideBar()
router.push(props.itemOrMenu.path)
} else $pub('reload-router-view')
}
}
</script>
<template>
<el-menu-item :index="itemOrMenu.path" @click="handleLink">
<vab-icon
v-if="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.badge" effect="dark" type="danger">
{{ itemOrMenu.meta.badge }}
</el-tag>
<span v-if="itemOrMenu.meta.dot" class="vab-dot vab-dot-error">
<span />
</span>
</el-menu-item>
</template>
<style lang="scss" scoped>
@use 'sass:math';
:deep(.el-tag) {
position: absolute;
right: 20px;
height: 16px;
padding-right: 4px;
padding-left: 4px;
// margin-top: #{math.div($base-menu-item-height - 16, 2)};
line-height: 16px;
border: 0;
}
.vab-dot {
position: absolute !important;
right: 20px;
// margin-top: #{math.div($base-menu-item-height - 6, 2)};
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { translate } from '@/i18n'
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
itemOrMenu: {
type: Object,
default() {
return null
},
},
})
const settingsStore: any = useSettingsStore()
const { theme }: any = storeToRefs(settingsStore)
</script>
<template>
<el-sub-menu
:index="itemOrMenu.path"
:teleported="theme.layout != 'horizontal'"
>
<template #title>
<vab-icon
v-if="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 />
</el-sub-menu>
</template>

View File

@@ -0,0 +1,83 @@
<template>
<component
:is="menuComponent"
v-if="!item.meta.hidden"
:item-or-menu="item"
>
<template v-if="item.children && item.children.length">
<vab-menu
v-for="route in item.children"
:key="route.path"
:item="route"
/>
</template>
</component>
</template>
<script lang="ts">
/* 防止偶发性自动导入失败 */
import { computed, defineComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useSettingsStore } from '@/store/modules/settings'
const imports = require.context('./components', true, /\.vue$/)
const Components: {
[key: string]: any
} = {}
imports.keys().forEach((key) => {
Components[key.replace(/(\/|\.|vue)/g, '')] = imports(key).default
})
export default defineComponent({
name: 'VabMenu',
components: Components,
props: {
item: {
type: Object,
required: true,
},
layout: {
type: String,
default: '',
},
},
setup(props) {
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const menuComponent = computed(() =>
props.item.children &&
props.item.children.some((_route: any) => {
return _route.meta.hidden !== true
})
? 'VabSubMenu'
: 'VabMenuItem'
)
return {
collapse,
menuComponent,
}
},
})
</script>
<style lang="scss" scoped>
.vab-menu-children-height {
height: 60vh !important;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
</style>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss">
.el-popper.is-light {
border: 0 !important;
}
</style>

View File

@@ -0,0 +1,165 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { translate } from '@/i18n'
import { isExternal } from '@/utils/validate'
import { openFirstMenu } from '@/config'
defineProps({
layout: {
type: String,
default: '',
},
})
const router = useRouter()
const routesStore: any = useRoutesStore()
const {
getTab: tab,
getTabMenu: tabMenu,
getRoutes: routes,
} = storeToRefs(routesStore)
const handleTabClick = () => {
nextTick(() => {
if (isExternal(tabMenu.value.path)) {
window.open(tabMenu.value.path)
setTimeout(() => {
router.push('/')
}, 1000)
} else if (openFirstMenu)
router.push(tabMenu.value.redirect || tabMenu.value)
})
}
</script>
<template>
<div class="vab-nav">
<el-row :gutter="15">
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="4">
<div class="left-panel">
<vab-fold v-if="layout !== 'float'" />
<el-tabs
v-if="layout === 'comprehensive'"
v-model="tab.data"
tab-position="top"
@tab-click="handleTabClick"
>
<template
v-for="(item, index) in routes"
:key="index + 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"
style="min-width: 16px"
/>
<span>
{{ translate(item.meta.title) }}
</span>
</template>
</el-tab-pane>
</template>
</el-tabs>
<vab-breadcrumb v-else class="hidden-xs-only" />
</div>
</el-col>
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="20">
<div class="right-panel">
<vab-error-log />
<vab-lock />
<vab-search />
<vab-notice />
<vab-full-screen />
<vab-language />
<vab-theme />
<vab-refresh />
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
.vab-nav {
position: relative;
height: $base-nav-height;
padding-right: $base-padding;
padding-left: $base-padding;
overflow: hidden;
user-select: none;
background: var(--el-color-white);
box-shadow: $base-box-shadow;
.left-panel {
display: flex;
align-items: center;
justify-items: center;
height: $base-nav-height;
:deep() {
.fold-unfold {
margin-right: $base-margin;
}
.el-tabs {
width: 100%;
margin-left: $base-margin;
.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 {
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: $base-nav-height;
:deep() {
[class*='ri-'] {
margin-left: $base-margin;
color: var(--el-color-grey);
cursor: pointer;
}
button {
[class*='ri-'] {
margin-left: 0;
color: var(--el-color-white);
cursor: pointer;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,356 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
import { getList } from '@/api/notice'
const $baseMessage: any = inject('$baseMessage')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const activeName = ref('notice')
const notices: any = ref([])
const badge = ref(undefined)
const fetchData = async () => {
const {
data: { list, total },
} = await getList()
notices.value = list
badge.value = total === 0 ? undefined : total
}
nextTick(() => {
if (theme.value.showNotice) fetchData()
})
const handleClick = () => {
fetchData()
}
const handleClearNotice = () => {
badge.value = undefined
notices.value = []
$baseMessage('清空消息成功', 'success', 'vab-hey-message-success')
}
const formatTime = (time: string) => {
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return date.toLocaleDateString()
}
</script>
<template>
<el-badge
v-if="theme.showNotice"
class="notice-badge"
type="danger"
:value="badge"
>
<el-popover
placement="bottom-end"
popper-class="notice-popover"
:show-arrow="false"
:width="380"
>
<template #reference>
<div class="notice-trigger">
<vab-icon icon="notification-line" />
</div>
</template>
<div class="notice-container">
<div class="notice-header">
<h3>{{ translate('消息中心') }}</h3>
<el-button
class="clear-btn"
size="small"
text
type="primary"
@click="handleClearNotice"
>
<vab-icon icon="delete-bin-line" />
{{ translate('清空') }}
</el-button>
</div>
<el-tabs
v-model="activeName"
class="notice-tabs"
@tab-click="handleClick"
>
<el-tab-pane :label="translate('通知')" name="notice">
<div class="notice-list">
<el-scrollbar height="320px">
<div
v-if="notices.length === 0"
class="empty-state"
>
<vab-icon
class="empty-icon"
icon="notification-off-line"
/>
<p>{{ translate('暂无通知') }}</p>
</div>
<div v-else class="notice-items">
<div
v-for="(item, index) in notices"
:key="index"
class="notice-item"
>
<div class="notice-avatar">
<el-avatar
:size="40"
:src="item.image"
/>
<div
v-if="!item.read"
class="notice-status"
></div>
</div>
<div class="notice-content">
<div
class="notice-text"
v-html="item.notice"
></div>
<div class="notice-time">
{{
formatTime(
item.time || new Date()
)
}}
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</el-tab-pane>
<el-tab-pane :label="translate('邮件')" name="email">
<div class="notice-list">
<el-scrollbar height="320px">
<div
v-if="notices.length === 0"
class="empty-state"
>
<vab-icon
class="empty-icon"
icon="mail-line"
/>
<p>{{ translate('暂无邮件') }}</p>
</div>
<div v-else class="notice-items">
<div
v-for="(item, index) in notices"
:key="index"
class="notice-item"
>
<div class="notice-avatar">
<el-avatar
:size="40"
:src="item.image"
/>
<div
v-if="!item.read"
class="notice-status"
></div>
</div>
<div class="notice-content">
<div class="notice-text">
{{ item.email }}
</div>
<div class="notice-time">
{{
formatTime(
item.time || new Date()
)
}}
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-popover>
</el-badge>
</template>
<style lang="scss" scoped>
.notice-badge {
:deep(.el-badge__content) {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
}
}
.notice-trigger {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.notice-container {
.notice-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.clear-btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
&:hover {
color: var(--el-color-danger);
background: var(--el-color-danger-light-9);
}
}
}
.notice-tabs {
:deep(.el-tabs__header) {
padding: 0 20px;
margin: 0;
.el-tabs__nav-wrap {
&::after {
display: none;
}
}
.el-tabs__item {
padding: 12px 16px;
font-size: 14px;
transition: all 0.3s ease;
&.is-active {
font-weight: 500;
color: var(--el-color-primary);
}
&:hover {
color: var(--el-color-primary);
}
}
.el-tabs__active-bar {
min-width: 28px;
}
}
:deep(.el-tabs__content) {
padding: 0;
}
}
.notice-list {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--el-text-color-placeholder);
.empty-icon {
margin-bottom: 12px;
font-size: 48px;
opacity: 0.6;
}
p {
margin: 0;
font-size: 14px;
}
}
.notice-items {
padding: 8px 0;
.notice-item {
display: flex;
align-items: flex-start;
padding: 12px 20px;
margin: 0 8px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
background: var(--el-fill-color-light);
}
.notice-avatar {
position: relative;
flex-shrink: 0;
margin-right: 12px;
.notice-status {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: var(--el-color-danger);
border: 2px solid #fff;
border-radius: 50%;
}
}
.notice-content {
flex: 1;
min-width: 0;
.notice-text {
margin-bottom: 4px;
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-primary);
word-break: break-all;
}
.notice-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
}
}
</style>
<style lang="scss">
.notice-popover {
padding: 0 !important;
border: 1px solid var(--el-border-color-lighter) !important;
border-radius: 12px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
.el-popover__title {
display: none;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<el-col :span="24">
<div class="bottom-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps({
span: {
type: Number,
default: 14,
},
})
</script>
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="left-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps({
span: {
type: Number,
default: 10,
},
})
</script>
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="right-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<el-col :span="24">
<div class="top-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,60 @@
<template>
<el-row class="vab-query-form" :gutter="0">
<slot />
</el-row>
</template>
<style lang="scss" scoped>
@use 'sass:math';
@mixin panel {
display: flex;
flex-wrap: wrap;
align-content: center;
align-items: center;
justify-content: flex-start;
min-height: $base-input-height;
margin: 0 0 #{math.div($base-margin, 2)} 0;
.el-form-item__content {
display: flex;
align-items: center;
}
> .el-button {
margin: 0 10px #{math.div($base-margin, 2)} 0 !important;
}
}
.vab-query-form {
:deep() {
.el-form-item:first-child {
margin: 0 0 #{math.div($base-margin, 2)} 0 !important;
}
.el-form-item + .el-form-item {
margin: 0 0 #{math.div($base-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>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const refreshRoute = () => {
$pub('reload-router-view')
}
</script>
<template>
<vab-icon
v-if="theme.showRefresh"
icon="refresh-line"
@click="refreshRoute"
/>
</template>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import VabProgress from 'nprogress'
import { useSettingsStore } from '@/store/modules/settings'
import { useTabsStore } from '@/store/modules/tabs'
import { handleActivePath } from '@/utils/routes'
import { keepAliveMaxNum } from '@/config'
import { VabRouteRecord } from '/#/router'
const route = useRoute()
const $sub: any = inject('$sub')
const $unsub: any = inject('$unsub')
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 updateKeepAliveNameList = (refreshRouteName = null) => {
keepAliveNameList.value = visitedRoutes.value
.filter(
(item: VabRouteRecord) =>
!item.meta.noKeepAlive && item.name !== refreshRouteName
)
.flatMap((item: VabRouteRecord) => item.name)
}
// 更新KeepAlive缓存页面
watchEffect(() => {
routerKey.value = handleActivePath(route, true)
updateKeepAliveNameList()
})
// 获取源码地址
$sub('get-code', () => {
window.open(
`https://github.com/zxwk2024/admin-plus/blob/main/${componentRef.value.$options.__source}`
)
})
$sub('reload-router-view', (refreshRouteName: any = 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)
})
// onUnmounted(() => {
// $unsub('get-code')
// $unsub('reload-router-view')
// })
</script>
<template>
<router-view v-slot="{ Component }">
<transition
mode="out-in"
:name="theme.showPageTransition ? 'fade-transform' : 'no-transform'"
>
<keep-alive :include="keepAliveNameList" :max="keepAliveMaxNum">
<component
:is="Component"
:key="routerKey"
ref="componentRef"
/>
</keep-alive>
</transition>
</router-view>
</template>

View File

@@ -0,0 +1,517 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useRoutesStore } from '@/store/modules/routes'
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
const MOD_KEY = isMac ? 'metaKey' : 'ctrlKey'
const show = ref(false)
const search = ref('')
const activeIndex = ref(0)
const inputRef = ref<HTMLInputElement | null>(null)
const routesStore = useRoutesStore()
const { getRoutes } = storeToRefs(routesStore)
const router = useRouter()
// 递归平铺所有菜单项
function flattenMenus(
routes: any[],
basePath = '',
parentIcon = undefined
): any[] {
let result: any[] = []
routes.forEach((route) => {
if (route.meta && route.meta.hidden) return
const fullPath = route.path.startsWith('/')
? route.path
: `${basePath}/${route.path}`
const icon =
route.meta && route.meta.icon ? route.meta.icon : parentIcon
if (route.meta && route.meta.title) {
result.push({
title: route.meta.title,
path: fullPath,
icon,
...route.meta,
})
}
if (route.children && route.children.length > 0) {
result = result.concat(
flattenMenus(route.children, fullPath, icon)
)
}
})
return result
}
const menuList = computed(() => flattenMenus(getRoutes.value))
// 搜索历史localStorage
const HISTORY_KEY = 'vab_search_history'
const searchHistory = ref<string[]>([])
function loadHistory() {
const raw = localStorage.getItem(HISTORY_KEY)
searchHistory.value = raw ? JSON.parse(raw) : []
}
function saveHistory(keyword: string) {
if (!keyword) return
let arr = searchHistory.value.filter((item) => item !== keyword)
arr.unshift(keyword)
if (arr.length > 8) arr = arr.slice(0, 8)
searchHistory.value = arr
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}
function removeHistory(keyword: string) {
searchHistory.value = searchHistory.value.filter(
(item) => item !== keyword
)
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value))
}
function clearHistory() {
searchHistory.value = []
localStorage.removeItem(HISTORY_KEY)
}
// 简单模糊搜索(可扩展为拼音/多字段)
const filteredMenus = computed(() => {
if (!search.value) return menuList.value
return menuList.value.filter(
(item) =>
item.title.toLowerCase().includes(search.value.toLowerCase()) ||
(item.path &&
item.path
.toLowerCase()
.includes(search.value.toLowerCase()))
)
})
function openSearch() {
show.value = true
search.value = ''
activeIndex.value = 0
loadHistory()
nextTick(() => {
inputRef.value?.focus()
})
}
function closeSearch() {
show.value = false
}
function selectMenu(index: number) {
const item = filteredMenus.value[index]
if (item && item.path) {
saveHistory(search.value)
closeSearch()
// 检查是否为外链
if (
item.target === '_blank' ||
item.path.startsWith('http://') ||
item.path.startsWith('https://') ||
item.path.startsWith('//')
) {
// 外链使用window.open
window.open(item.path, '_blank')
} else {
// 内部路由使用router.push
router.push(item.path)
}
}
}
function selectHistory(keyword: string) {
search.value = keyword
activeIndex.value = 0
nextTick(() => {
inputRef.value?.focus()
})
}
function onKeydown(e: KeyboardEvent) {
if (show.value) {
if (e.key === 'Escape') {
closeSearch()
} else if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex.value =
(activeIndex.value + 1) % filteredMenus.value.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex.value =
(activeIndex.value - 1 + filteredMenus.value.length) %
filteredMenus.value.length
} else if (e.key === 'Enter') {
selectMenu(activeIndex.value)
}
} else {
if ((e as any)[MOD_KEY] && e.key.toLowerCase() === 'k') {
e.preventDefault()
openSearch()
}
}
}
onMounted(() => {
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeydown)
})
function onItemClick(idx: number) {
selectMenu(idx)
}
function onInputKeydown(e: KeyboardEvent) {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'Enter' ||
e.key === 'Escape'
) {
// 交由全局keydown处理
e.preventDefault()
}
}
</script>
<template>
<span class="vab-search-trigger hidden-xs-only">
<vab-icon
icon="search-line"
title="菜单搜索 ({{ isMac ? '⌘+K' : 'Ctrl+K' }})"
@click="openSearch"
/>
</span>
<teleport to="body">
<transition name="fade">
<div
v-if="show"
class="vab-spotlight-mask"
@click.self="closeSearch"
>
<div class="vab-spotlight-card">
<div class="vab-spotlight-input-wrap">
<vab-icon
class="vab-spotlight-input-icon"
icon="search-line"
/>
<input
ref="inputRef"
v-model="search"
autocomplete="off"
class="vab-spotlight-input"
:placeholder="
isMac ? '搜索菜单… (⌘+K)' : '搜索菜单… (Ctrl+K)'
"
@keydown="onInputKeydown"
/>
</div>
<div
v-if="searchHistory.length"
class="vab-spotlight-history-wrap"
>
<div class="vab-spotlight-history">
<div class="vab-spotlight-history-title">
搜索历史
<span
class="vab-spotlight-history-clear"
@click="clearHistory"
>
清空
</span>
</div>
<div class="vab-spotlight-history-list">
<span
v-for="item in searchHistory"
:key="item"
class="vab-spotlight-history-item"
>
<span @click="selectHistory(item)">
{{ item }}
</span>
<vab-icon
class="vab-spotlight-history-del"
icon="close-circle-fill"
@click.stop="removeHistory(item)"
/>
</span>
</div>
</div>
</div>
<ul class="vab-spotlight-list">
<li
v-for="(item, idx) in filteredMenus"
:key="item.path + item.title"
:class="[
'vab-spotlight-item',
{ active: idx === activeIndex },
]"
@mousedown.prevent="onItemClick(idx)"
@mouseenter="activeIndex = idx"
>
<vab-icon
v-if="item.icon"
class="vab-spotlight-item-icon"
:icon="item.icon"
/>
<div class="vab-spotlight-item-content">
<div class="vab-spotlight-item-title">
{{ item.title }}
</div>
<div class="vab-spotlight-item-path">
{{ item.path }}
</div>
</div>
</li>
<li
v-if="filteredMenus.length === 0"
class="vab-spotlight-empty"
>
<div v-if="!search">请输入关键词进行搜索</div>
<div v-else>无匹配菜单</div>
</li>
</ul>
</div>
</div>
</transition>
<button
class="vab-spotlight-fab"
title="菜单搜索 ({{ isMac ? '⌘+K' : 'Ctrl+K' }})"
@click="openSearch"
>
<vab-icon icon="search-line" />
</button>
</teleport>
</template>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.vab-spotlight-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(30, 34, 45, 0.25);
backdrop-filter: blur(2px);
}
.vab-spotlight-card {
display: flex;
flex-direction: column;
width: 600px;
max-width: 96vw;
padding: 0 0 8px 0;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 18px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18);
backdrop-filter: blur(12px) saturate(120%);
animation: fadeInUp 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: none;
}
}
.vab-spotlight-input-wrap {
display: flex;
align-items: center;
padding: 24px 28px 12px 28px;
}
.vab-spotlight-input-icon {
margin-right: 10px;
font-size: 22px;
color: #666;
}
.vab-spotlight-input {
flex: 1;
padding: 8px 0;
font-size: 20px;
color: #222;
outline: none;
background: transparent;
border: none;
}
.vab-spotlight-list {
max-height: 340px;
padding: 0 8px 0 8px;
margin: 0;
overflow-y: auto;
list-style: none;
}
.vab-spotlight-item {
display: flex;
align-items: center;
padding: 12px 18px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 10px;
transition: background 0.18s;
&:last-child {
margin-bottom: 0;
}
&.active,
&:hover {
background: #f5f5f5;
}
}
.vab-spotlight-item-icon {
margin-right: 14px;
font-size: 26px !important;
color: #666;
}
.vab-spotlight-item-content {
display: flex;
flex-direction: column;
min-width: 0;
}
.vab-spotlight-item-title {
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
line-height: 1.2;
color: #222;
white-space: nowrap;
}
.vab-spotlight-item-path {
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
color: #8c8c8c;
white-space: nowrap;
}
.vab-spotlight-empty {
padding: 32px 0 24px 0;
font-size: 15px;
color: #aaa;
text-align: center;
}
.vab-spotlight-fab {
position: fixed;
right: 32px;
bottom: 32px;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
font-size: 28px;
color: #fff;
cursor: pointer;
outline: none;
background: #666;
border: none;
border-radius: 50%;
box-shadow: 0 4px 16px 0 rgba(102, 102, 102, 0.18);
transition:
background 0.18s,
box-shadow 0.18s,
transform 0.18s;
&:hover {
background: #888;
box-shadow: 0 8px 32px 0 rgba(102, 102, 102, 0.18);
transform: translateY(-2px) scale(1.06);
}
&:active {
background: #555;
transform: scale(0.98);
}
}
@media (max-width: 600px) {
.vab-spotlight-card {
width: 98vw;
min-width: 0;
padding: 0 0 8px 0;
}
.vab-spotlight-input-wrap {
padding: 18px 10px 10px 10px;
}
.vab-spotlight-fab {
right: 16px;
bottom: 16px;
width: 48px;
height: 48px;
font-size: 22px;
}
}
.vab-search-trigger {
display: inline-flex;
align-items: center;
font-size: 22px;
color: #666;
cursor: pointer;
transition: color 0.18s;
&:hover {
color: #666;
}
}
.vab-spotlight-history-wrap {
padding: 0 28px 0 28px;
margin-bottom: 8px;
}
.vab-spotlight-history-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
font-size: 13px;
color: #888;
}
.vab-spotlight-history-clear {
margin-left: 8px;
font-size: 12px;
color: #aaa;
cursor: pointer;
&:hover {
color: #ff4d4f;
}
}
.vab-spotlight-history-list {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-bottom: 8px;
}
.vab-spotlight-history-item {
display: inline-flex;
align-items: center;
padding: 2px 12px 2px 10px;
font-size: 14px;
color: #666;
cursor: pointer;
background: #f5f7fa;
border-radius: 16px;
transition:
background 0.18s,
color 0.18s;
&:hover {
color: #666;
background: #f5f5f5;
}
}
.vab-spotlight-history-del {
margin-left: 4px;
font-size: 14px;
color: #bbb;
cursor: pointer;
&:hover {
color: #ff4d4f;
}
}
</style>

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { useRoutesStore } from '@/store/modules/routes'
import { useSettingsStore } from '@/store/modules/settings'
import { defaultOpeneds, uniqueOpened } from '@/config'
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(() => {
return props.layout === 'comprehensive'
? partialRoutes.value
: routes.value.flatMap((route: any) =>
route.meta.levelHidden && route.children
? [...route.children]
: route
)
})
</script>
<template>
<el-scrollbar
class="vab-side-bar"
:class="{
'is-collapse': collapse,
'side-bar-common': layout === 'common',
}"
>
<vab-logo
v-if="
layout === 'vertical' ||
layout === 'comprehensive' ||
layout === 'float'
"
/>
<el-menu
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:collapse="collapse"
:collapse-transition="false"
:default-active="activeMenu.data"
:default-openeds="defaultOpeneds"
menu-trigger="click"
mode="vertical"
:text-color="variables['menu-color']"
:unique-opened="uniqueOpened"
>
<template
v-for="(item, index) in handleRoutes"
:key="index + item.name"
>
<vab-menu v-if="!item.meta.hidden" :item="item" />
</template>
</el-menu>
</el-scrollbar>
</template>
<style lang="scss" scoped>
@mixin active {
&:hover {
color: var(--el-color-white);
background-color: $base-menu-active;
}
&.is-active {
color: var(--el-color-white);
background-color: $base-menu-active;
}
}
.vab-side-bar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: $base-z-index + 1;
width: var(--el-left-menu-width);
height: 100vh;
overflow: hidden;
background: $base-menu-background;
box-shadow: $base-box-shadow;
transition: $base-transition;
&.side-bar-common {
top: $base-header-height;
height: calc(100vh - #{$base-header-height});
}
&.is-collapse {
width: $base-left-menu-width-min;
border-right: 0;
:deep() {
.el-menu--collapse.el-menu {
> .el-menu-item,
> .el-sub-menu {
text-align: center;
.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-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu-item,
.el-sub-menu__title {
height: $base-menu-item-height;
overflow: hidden;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
i {
color: inherit;
}
}
.el-menu-item {
@include active;
}
}
}
</style>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss">
@mixin menuActiveHover {
&:hover,
&.is-active {
i {
color: var(--el-color-white);
}
color: var(--el-color-white);
background: var(--el-color-primary);
.el-sub-menu__title {
i {
color: var(--el-color-white);
}
color: var(--el-color-white);
background: var(--el-color-primary);
}
}
}
.el-menu {
border-right: 0;
}
.el-popper {
.el-menu--vertical {
.el-menu-item,
.el-sub-menu {
height: $base-menu-item-height;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include menuActiveHover;
i {
color: inherit;
}
.el-sub-menu__title {
height: $base-menu-item-height;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include menuActiveHover;
}
}
}
}
</style>

View File

@@ -0,0 +1,614 @@
<script lang="ts" setup>
import { useTabsStore } from '@/store/modules/tabs'
import { useRoutesStore } from '@/store/modules/routes'
import { useSettingsStore } from '@/store/modules/settings'
import { handleActivePath, handleTabs } from '@/utils/routes'
import { translate } from '@/i18n'
import { VabRoute, VabRouteRecord } from '/#/router'
defineProps({
layout: {
type: String,
default: '',
},
})
const $pub: any = inject('$pub')
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const routesStore = useRoutesStore()
const { getRoutes: routes } = storeToRefs(routesStore)
const tabsStore = useTabsStore()
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
const {
addVisitedRoute,
delVisitedRoute,
delOthersVisitedRoutes,
delLeftVisitedRoutes,
delRightVisitedRoutes,
delAllVisitedRoutes,
} = tabsStore
const tabActive = ref('')
const active = ref(false)
const hoverRoute = ref()
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const isActive = (path: string) => path === handleActivePath(route, true)
const isNoCLosable = (tag: any) => tag.meta.noClosable
const handleTabClick = (tab: any) => {
if (!isActive(tab.name)) router.push(visitedRoutes.value[tab.index])
}
const handleVisibleChange = (val: boolean) => {
active.value = val
}
const initNoCLosableTabs = (routes: VabRouteRecord[]) => {
routes.forEach((_route) => {
if (_route.meta.noClosable) addTabs(_route)
if (_route.children) initNoCLosableTabs(_route.children)
})
}
/**
* 添加标签页
* @param tag route
* @returns {Promise<void>}
*/
const addTabs = async (tag: VabRoute | VabRouteRecord) => {
const tab = handleTabs(tag)
if (tab) {
await addVisitedRoute(tab)
tabActive.value = tab.path
}
}
/**
* 根据原生路径删除标签中的标签
* @param rawPath 原生路径
* @returns {Promise<void>}
*/
const handleTabRemove: any = async (rawPath: string) => {
if (isActive(rawPath)) await toLastTab()
await delVisitedRoute(rawPath)
}
const handleCommand = (command: string) => {
switch (command) {
case 'refreshThisTab':
refreshThisTab()
break
case 'closeOthersTabs':
closeOthersTabs()
break
case 'closeLeftTabs':
closeLeftTabs()
break
case 'closeRightTabs':
closeRightTabs()
break
case 'closeAllTabs':
closeAllTabs()
break
}
}
/**
* 刷新当前标签页
* @returns {Promise<void>}
*/
const refreshThisTab = () => {
$pub('reload-router-view')
}
/**
* 删除其他标签页
* @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
.filter((_: any) => _.path !== handleActivePath(route, true))
.slice(-1)[0]
if (latestView) await router.push(latestView)
else await router.push('/')
}
const { x, y } = useMouse()
const openMenu = () => {
left.value = x.value
top.value = y.value
visible.value = true
}
const closeMenu = () => {
visible.value = false
hoverRoute.value = null
}
initNoCLosableTabs(routes.value)
watch(
() => route.fullPath,
() => {
addTabs(route)
},
{
immediate: true,
}
)
watchEffect(() => {
if (visible.value) document.body.addEventListener('click', closeMenu)
else document.body.removeEventListener('click', closeMenu)
})
</script>
<template>
<div class="vab-tabs">
<vab-fold v-if="layout === 'common'" />
<el-tabs
v-model="tabActive"
class="vab-tabs-content"
:class="{
['vab-tabs-content-' + theme.tabsBarStyle]: true,
}"
type="card"
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
>
<el-tab-pane
v-for="item in visitedRoutes"
:key="item.path"
:closable="!isNoCLosable(item)"
:name="item.path"
>
<template #label>
<span
style="display: inline-block"
@contextmenu.prevent="openMenu"
>
<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="item.meta && item.meta.title">
{{ translate(item.meta.title) }}
</span>
</span>
</template>
</el-tab-pane>
</el-tabs>
<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="refreshThisTab">
<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-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="refreshThisTab">
<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>
</div>
</template>
<style lang="scss" scoped>
@use 'sass:math';
.vab-tabs {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
min-height: $base-tabs-height;
padding-right: $base-padding;
padding-left: $base-padding;
user-select: none;
background: var(--el-color-white);
border-top: 1px solid #f6f6f6;
:deep() {
.fold-unfold {
margin-right: $base-margin;
}
[class*='ri'] {
margin-right: 3px;
}
.vab-icon {
vertical-align: -3px;
}
}
&-content {
width: calc(100% - 40px);
&-card {
height: #{$base-tag-item-height};
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height};
line-height: #{$base-tag-item-height};
}
.el-tabs__header {
margin: 0 0 1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
box-sizing: border-box;
height: #{$base-tag-item-height};
margin-top: 1px;
margin-right: 5px;
line-height: #{$base-tag-item-height};
border: 1px solid #{$base-border-color} !important;
border-radius: #{$base-border-radius};
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-active {
color: var(--el-color-primary);
outline: none;
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary);
}
&:hover {
border: 1px solid var(--el-color-primary);
}
}
}
}
}
&-smart {
height: #{$base-tag-item-height};
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height};
line-height: #{$base-tag-item-height};
}
.el-tabs__header {
margin: 0 0 1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
height: #{$base-tag-item-height};
margin-top: 1px;
margin-right: 5px;
line-height: #{$base-tag-item-height};
outline: none;
border: 0;
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-active {
outline: none;
background: var(--el-color-primary-light-9);
&:after {
width: 100%;
transition: #{$base-transition};
}
}
&:after {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
content: '';
background-color: var(--el-color-primary);
transition: #{$base-transition};
}
&:hover {
background: var(--el-color-primary-light-9);
&:after {
width: 100%;
transition: #{$base-transition};
}
}
}
}
}
}
&-smooth {
height: #{$base-tag-item-height} + 4;
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height} + 14;
line-height: #{$base-tag-item-height} + 14;
}
.el-tabs__header {
margin: 0 0 -1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
height: #{$base-tag-item-height} + 4;
padding: 0 30px 0 30px;
margin-top: math.div(
$base-tabs-height - $base-tag-item-height - 3.1,
2
);
margin-right: -18px;
line-height: #{$base-tag-item-height} + 4;
text-align: center;
outline: none;
border: 0;
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-closable:hover {
padding: 0 30px 0 30px;
}
&.is-active {
padding: 0 30px 0 30px;
color: var(--el-color-primary);
outline: none;
background: var(--el-color-primary-light-9);
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
&:hover {
padding: 0 30px 0 30px;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
}
&.is-closable {
padding: 0 30px 0 30px;
}
}
&:hover {
padding: 0 30px 0 30px;
color: var(--el-color-black);
background: #dee1e6;
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
}
}
}
}
}
}
.contextmenu {
position: fixed;
top: 0;
left: 0;
z-index: 10;
.el-dropdown-menu__item:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
}
&-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: #9a9a9a;
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: #9a9a9a;
}
&:after {
position: absolute;
top: 2px;
left: 8px;
width: 6px;
height: 6px;
content: '';
background: #9a9a9a;
}
}
.box-t {
&:before {
transition: transform 0.3s ease-out 0.3s;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,515 @@
<script lang="ts" setup>
import { translate } from '@/i18n'
import { useSettingsStore } from '@/store/modules/settings'
const $sub: any = inject('$sub')
const $unsub: any = inject('$unsub')
const $baseLoading: any = inject('$baseLoading')
const settingsStore: any = useSettingsStore()
const { theme, device }: any = storeToRefs(settingsStore)
const { saveTheme, resetTheme, updateTheme }: any = settingsStore
const state = reactive({
drawerVisible: false,
})
const handleOpenTheme = () => {
state.drawerVisible = true
}
const updateMenuWidth = () => {
useCssVar('--el-left-menu-width', ref(null)).value =
theme.value.menuWidth
}
const setDefaultTheme = async () => {
await resetTheme()
await updateTheme()
state.drawerVisible = false
if (document.body.getBoundingClientRect().width - 1 < 992)
location.reload()
}
const handleSaveTheme = async () => {
await saveTheme()
state.drawerVisible = false
if (document.body.getBoundingClientRect().width - 1 < 992)
location.reload()
}
const shuffle = (val: string | boolean, list: (string | boolean)[]) =>
list.filter((_) => _ !== val)[(Math.random() * (list.length - 1)) | 0]
const randomTheme = async () => {
const loading = $baseLoading(0)
// 随机换肤重置移除主题,防止代码更新影响样式
await resetTheme()
theme.value.themeName = shuffle(theme.value.themeName, [
'blue-black',
'blue-white',
'ocean',
'green-black',
'green-white',
'purple-black',
'purple-white',
'red-black',
'red-white',
])
theme.value.columnStyle = shuffle(theme.value.columnStyle, [
'vertical',
'horizontal',
'card',
'arrow',
])
// theme.value.background = shuffle(theme.value.background, [
// 'none',
// 'vab-background',
// ])
theme.value.tabsBarStyle = shuffle(theme.value.tabsBarStyle, [
'card',
'smart',
'smooth',
])
theme.value.showTabsIcon = shuffle(theme.value.showTabsIcon, [
true,
false,
])
theme.value.layout =
device.value === 'desktop'
? shuffle(theme.value.layout, [
'horizontal',
'vertical',
'column',
'comprehensive',
'common',
'float',
])
: 'vertical'
try {
await updateTheme()
await saveTheme()
} finally {
setTimeout(() => {
loading.close()
}, 1000)
}
}
$sub('theme', () => {
handleOpenTheme()
})
$sub('random-theme', () => {
randomTheme()
})
onBeforeUnmount(() => {
$unsub('theme')
$unsub('random-theme')
})
</script>
<template>
<el-drawer
v-model="state.drawerVisible"
append-to-body
class="vab-drawer"
direction="rtl"
size="285px"
:title="translate('主题配置')"
>
<el-scrollbar height="88vh">
<el-form ref="form" label-position="left" :model="theme">
<el-form-item>
<template #label>
{{ translate('布局') }}
<el-tooltip
:content="
translate(
'布局配置仅' +
'在电脑视窗下生效,手机视窗时将' +
'默认锁定为纵向布局'
)
"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.layout"
:disabled="device === 'mobile'"
placeholder="请选择"
>
<el-option
key="column"
:label="translate('分栏')"
value="column"
/>
<el-option
key="comprehensive"
:label="translate('综合')"
value="comprehensive"
/>
<el-option
key="vertical"
:label="translate('纵向')"
value="vertical"
/>
<el-option
key="horizontal"
:label="translate('横向')"
value="horizontal"
/>
<el-option
key="common"
:label="translate('常规')"
value="common"
/>
<el-option
key="float"
:label="translate('浮动')"
value="float"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('主题')">
<el-select v-model="theme.themeName" @change="updateTheme">
<el-option
key="blue-black"
:label="translate('蓝黑')"
value="blue-black"
/>
<el-option
key="blue-white"
:label="translate('蓝白')"
value="blue-white"
/>
<el-option
key="green-black"
:label="translate('绿黑')"
value="green-black"
/>
<el-option
key="green-white"
:label="translate('绿白')"
value="green-white"
/>
<el-option
key="purple-black"
:label="translate('紫黑')"
value="purple-black"
/>
<el-option
key="purple-white"
:label="translate('紫白')"
value="purple-white"
/>
<!-- 红黑红白主题完成群文档任务免费获取 -->
<el-option
key="red-black"
:label="translate('红黑')"
value="red-black"
/>
<el-option
key="red-white"
:label="translate('红白')"
value="red-white"
/>
<el-option
key="ocean"
:label="translate('渐变')"
value="ocean"
/>
</el-select>
</el-form-item>
<el-form-item class="vab-item-custom">
<template #label>
{{ translate('菜单背景') }}
<el-tooltip
:content="
translate(
'支持纵向布局' +
'、分栏布局、综合' +
'布局、常规布局,不支持横' +
'向布局、浮动' +
'布局'
)
"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-radio-group
v-model="theme.background"
:disabled="
theme.layout === 'float' ||
theme.layout === 'horizontal'
"
@change="updateTheme"
>
<el-radio-button
class="none"
label="none"
value="none"
/>
<el-radio-button
class="vab-background"
label="vab-background"
value="vab-background"
/>
</el-radio-group>
</el-form-item>
<el-form-item :label="translate('菜单宽度')">
<el-select
v-model="theme.menuWidth"
:disabled="
theme.layout === 'float' ||
theme.layout === 'horizontal'
"
@change="updateMenuWidth"
>
<el-option key="266px" label="266px" value="266px" />
<el-option key="277px" label="277px" value="277px" />
<el-option key="288px" label="288px" value="288px" />
</el-select>
</el-form-item>
<el-form-item :label="translate('标签')">
<el-switch v-model="theme.showTabs" />
</el-form-item>
<el-form-item>
<template #label>
{{ translate('标签图标') }}
<el-tooltip
:content="translate('标签开启时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-switch
v-model="theme.showTabsIcon"
:disabled="!theme.showTabs"
/>
</el-form-item>
<el-form-item>
<template #label>
{{ translate('标签风格') }}
<el-tooltip
:content="translate('标签开启时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.tabsBarStyle"
:disabled="!theme.showTabs"
>
<el-option
key="card"
:label="translate('卡片')"
value="card"
/>
<el-option
key="smart"
:label="translate('灵动')"
value="smart"
/>
<el-option
key="smooth"
:label="translate('圆滑')"
value="smooth"
/>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
{{ translate('分栏风格') }}
<el-tooltip
:content="translate('分栏布局时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.columnStyle"
:disabled="theme.layout !== 'column'"
>
<el-option
key="vertical"
:label="translate('纵向')"
value="vertical"
/>
<el-option
key="horizontal"
:label="translate('横向')"
value="horizontal"
/>
<el-option
key="card"
:label="translate('卡片')"
value="card"
/>
<el-option
key="arrow"
:label="translate('箭头')"
value="arrow"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('头部固定')">
<el-switch
v-model="theme.fixedHeader"
:disabled="theme.layout === 'common'"
/>
</el-form-item>
<el-form-item :label="translate('国际化')">
<el-switch v-model="theme.showLanguage" />
</el-form-item>
<el-form-item :label="translate('进度条')">
<el-switch v-model="theme.showProgressBar" />
</el-form-item>
<el-form-item :label="translate('刷新')">
<el-switch v-model="theme.showRefresh" />
</el-form-item>
<el-form-item :label="translate('搜索')">
<el-switch v-model="theme.showSearch" />
</el-form-item>
<el-form-item :label="translate('通知')">
<el-switch v-model="theme.showNotice" />
</el-form-item>
<el-form-item :label="translate('全屏')">
<el-switch v-model="theme.showFullScreen" />
</el-form-item>
<el-form-item :label="translate('锁屏')">
<el-switch v-model="theme.showLock" />
</el-form-item>
<el-form-item :label="translate('页面动画')">
<el-switch v-model="theme.showPageTransition" />
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<el-button type="primary" @click="handleSaveTheme">
{{ translate('保存') }}
</el-button>
<el-button @click="setDefaultTheme">
{{ translate('恢复默认') }}
</el-button>
</template>
</el-drawer>
</template>
<style lang="scss">
@use 'sass:math';
.vab-drawer {
// position: relative;
.el-drawer__header {
padding: $base-padding $base-padding 0 $base-padding;
margin-bottom: 0;
}
.el-drawer__body {
padding-right: 0;
.el-scrollbar__wrap {
height: calc(100vh - 80px);
padding-right: $base-padding;
.el-divider--horizontal {
margin: $base-margin * 2 0 $base-margin * 2 0;
}
.el-form-item {
display: flex;
align-items: center;
&__label {
flex: 1 1;
i {
cursor: pointer;
}
}
&__content {
flex: 0 0 auto;
}
&.vab-item-custom {
display: block !important;
height: 130px;
}
.el-radio-button {
float: left;
display: block;
width: 80px;
height: 80px;
margin: 10px;
cursor: pointer;
border-radius: 5px;
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
&.is-active {
box-shadow: 0 0 2px 2px #1890ff;
}
.el-radio-button__orig-radio,
.el-radio-button__inner {
display: none;
}
&.none {
font-family: 'remixicon', sans-serif !important;
font-size: 16px;
font-weight: 580;
line-height: 80px;
text-align: center;
background: #f7f7f7 none;
background-size: cover;
&:before {
content: '\eace';
}
}
&.vab-background {
background: url(~@/assets/theme_images/background-1.png);
background-size: cover;
}
}
.el-input__inner {
width: 115px;
}
}
}
}
.el-drawer__footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index + 1;
display: flex;
padding: math.div($base-padding, 2);
background: var(--el-color-white);
border-top: 1px solid #{$base-border-color};
}
}
</style>

View File

@@ -0,0 +1,168 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
const $pub: any = inject('$pub')
//const $baseMessage: any = inject('$baseMessage')
const settingsStore: any = useSettingsStore()
const { theme }: any = storeToRefs(settingsStore)
const handleOpenTheme = () => {
$pub('theme')
}
const randomTheme = () => {
$pub('random-theme')
}
const buy = () => {
window.open('https://vuejs-core.cn/authorization')
}
// const getCode = () => {
// if (process.env.NODE_ENV === 'development') $pub('get-code')
// else
// ElMessageBox.prompt(
// '请输入密码(密码在购买时获得跳转后需登录购买时绑定的github账号)',
// '温馨提示',
// {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// }
// )
// .then(({ value }) => {
// if (value !== 'vabp') {
// $baseMessage(
// '密码不正确!',
// 'error',
// 'vab-hey-message-error'
// )
// return
// }
// $pub('get-code')
// })
// .catch(() => {})
// }
const removeLocalStorage = () => {
localStorage.clear()
location.reload()
}
</script>
<template>
<ul v-if="theme.showThemeSetting" class="vab-theme-setting">
<li @click="handleOpenTheme">
<a>
<vab-icon icon="brush-2-line" />
<p>{{ translate('主题配置') }}</p>
</a>
</li>
<li @click="randomTheme">
<a>
<vab-icon icon="apps-line" />
<p>{{ translate('随机换肤') }}</p>
</a>
</li>
<li @click="buy">
<a>
<vab-icon icon="shopping-cart-2-line" />
<p>{{ translate('购买源码') }}</p>
</a>
</li>
<!-- <li @click="getCode">
<a>
<vab-icon icon="file-copy-line" />
<p>{{ translate('拷贝源码') }}</p>
</a>
</li> -->
<li @click="removeLocalStorage">
<a>
<vab-icon icon="delete-bin-4-line" />
<p>
{{ translate('清理缓存') }}
</p>
</a>
</li>
</ul>
</template>
<style lang="scss" scoped>
.vab-theme-setting {
position: fixed;
top: 50%;
right: 0;
z-index: $base-z-index - 2;
padding: 10px 0 0 0;
margin: 0;
text-align: center;
cursor: pointer;
background: var(--el-color-white);
border: 1px solid #{$base-border-color};
border-top-left-radius: $base-border-radius + 3;
border-bottom-left-radius: $base-border-radius + 3;
box-shadow: 0 0 50px 0 rgb(82 63 105 / 15%);
transform: translateY(-50%);
> li {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px 10px 10px;
margin: 0;
list-style: none;
&:nth-child(2) {
[class*='ri-'] {
animation: rotate 6s linear infinite;
}
}
$colors: (
1: #3698fd,
2: #1bc3bb,
3: #faa500,
4: #b37feb,
5: #ef4c5d,
);
@each $key, $color in $colors {
&:nth-child(#{$key}) {
a {
color: $color;
background: mix($base-color-white, $color, 90%);
transition:
color 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease,
-webkit-box-shadow 0.15s ease;
&:hover {
color: var(--el-color-white);
background: $color;
}
}
}
}
a {
display: inline-block;
width: 60px;
height: 60px;
padding-top: 10px;
text-align: center;
background: #f6f8f9;
border-radius: $base-border-radius + 3;
p {
padding: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: $base-font-size-small;
line-height: 25px;
white-space: nowrap;
}
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const handleOpenTheme = () => {
$pub('theme')
}
</script>
<template>
<span v-if="theme.showTheme">
<vab-icon icon="brush-2-line" @click="handleOpenTheme" />
</span>
</template>

39
library/index.ts Normal file
View File

@@ -0,0 +1,39 @@
import { App } from 'vue'
// 加载雪碧图
import '@/icon'
// 加载全局样式样式
import './styles/vab.scss'
import { createHead } from '@vueuse/head'
// 加载Icon
import VabIcon from 'vab-icons'
import 'vab-icons/lib/vab-icons.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const name = process['env']['VUE_' + 'APP_' + 'GITHUB_' + 'USER_' + 'NAME']
const noTest = name !== 'test'
const noEmpty = name !== 'undefined'
const dev = process['env']['NODE_' + 'ENV'] === 'dev' + 'elop' + 'ment'
export function setupVab(app: App<Element>) {
if ((noTest && noEmpty && !dev && VabIcon) || (dev && VabIcon)) {
app.use(createHead())
app.component('VabIcon', VabIcon)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 加载背景
const Themes = require.context('./styles/background', false, /\.scss$/)
Themes.keys().map(Themes)
// 加载插件
const Plugins = require.context('./plugins', true, /\.ts$/)
Plugins.keys().forEach((key) => {
app.use(Plugins(key).default)
})
}
}

View File

@@ -0,0 +1,78 @@
<!--分栏布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
</script>
<template>
<div
class="vab-layout-column"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-column-bar />
<div
class="vab-main"
:class="{
['vab-main-' + theme.columnStyle]: true,
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-column {
.vab-main {
&.is-collapse-main {
&.vab-main-horizontal {
margin-left: $base-left-menu-width-min * 1.3;
:deep() {
.fixed-header {
width: calc(
100% - #{$base-left-menu-width-min} * 1.3
);
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<!--常规布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-common"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-header layout="common" />
<div>
<vab-side-bar layout="common" />
<div
v-show="showTabs"
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<vab-tabs layout="common" />
</div>
</div>
</div>
<div
class="vab-main main-padding"
:class="{
'is-collapse-main': collapse,
}"
>
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-common {
:deep() {
.vab-tabs-content {
width: calc(
100% - 60px - #{$base-font-size-default} -
#{$base-padding} - 2px
) !important;
}
.vab-header {
.vab-main {
width: 100%;
margin: auto $base-margin;
}
}
}
}
</style>

View File

@@ -0,0 +1,58 @@
<!--综合布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-comprehensive"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar layout="comprehensive" />
<div
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav layout="comprehensive" />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>

View File

@@ -0,0 +1,98 @@
<!--浮动布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
})
const settingsStore = useSettingsStore()
const { foldSideBar, openSideBar } = settingsStore
foldSideBar()
onUnmounted(() => {
openSideBar()
})
</script>
<template>
<div
class="vab-layout-float"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar layout="float" />
<div class="vab-main">
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav layout="float" />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss" scoped>
.vab-layout-float {
:deep() {
.vab-main {
margin-left: $base-left-menu-width-min !important;
.fixed-header {
width: $base-right-content-width-min !important;
}
}
.el-menu--collapse.el-menu li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
.vab-menu-children-height {
height: auto !important;
}
.el-menu {
&--vertical {
.el-menu--popup-right-start {
width: 335px !important;
.el-sub-menu__title,
.el-menu-item {
float: left;
width: 160px;
min-width: 160px;
margin: 0 0 5px 5px;
border-radius: $base-border-radius;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 横向布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-horizontal"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-header layout="horizontal" />
<div
v-show="showTabs"
:class="{
'vab-tabs-horizontal': showTabs,
}"
>
<div class="vab-main">
<vab-tabs />
</div>
</div>
</div>
<div class="vab-main main-padding">
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-horizontal {
:deep() {
.vab-main {
width: 92% !important;
margin: auto !important;
}
}
.vab-tabs-horizontal {
background: var(--el-color-white);
box-shadow: $base-box-shadow;
}
.vab-nav {
.fold-unfold {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<!-- 纵向布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
const settingsStore = useSettingsStore()
const { foldSideBar } = settingsStore
</script>
<template>
<div
class="vab-layout-vertical"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar />
<div
v-if="device === 'mobile' && !collapse"
class="v-modal"
@click="foldSideBar"
/>
<div
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>

162
library/layouts/index.vue Normal file
View File

@@ -0,0 +1,162 @@
<template>
<div class="vue-admin-better-wrapper" :class="{ mobile }">
<component
:is="'vab-layout-' + theme.layout"
:collapse="collapse"
:device="device"
:fixed-header="theme.fixedHeader"
:show-tabs="theme.showTabs"
/>
<el-backtop target="#app" />
<!-- 主题组件放到layouts下防止主题切换导致主题组件重新加载 -->
<vab-theme-drawer />
<vab-theme-setting />
</div>
</template>
<script lang="ts">
import { useSettingsStore } from '@/store/modules/settings'
const imports = require.context('./', true, /\.vue$/)
const Components: any = {}
imports
.keys()
.filter((key) => key !== './index.vue')
.forEach((key) => {
Components[key.replace(/(\/|\.|index.vue)/g, '')] =
imports(key).default
})
export default defineComponent({
name: 'Layouts',
components: Components,
setup() {
const settingsStore = useSettingsStore()
const { device, collapse, theme } = storeToRefs(settingsStore)
const { toggleDevice, foldSideBar, openSideBar, updateTheme } =
settingsStore
const mobile = ref(false)
let oldLayout = theme.value.layout
const resizeBody = () => {
mobile.value =
document.body.getBoundingClientRect().width - 1 < 992
}
watch(mobile, (val) => {
if (val) {
oldLayout = theme.value.layout
foldSideBar()
} else openSideBar()
theme.value.layout = val ? 'vertical' : oldLayout
toggleDevice(val ? 'mobile' : 'desktop')
})
resizeBody()
updateTheme()
const cleanup = useEventListener('resize', () => {
resizeBody()
})
onUnmounted(() => {
if (mobile) theme.value.layout = oldLayout
cleanup()
})
return {
theme,
device,
mobile,
collapse,
foldSideBar,
openSideBar,
toggleDevice,
}
},
})
</script>
<style lang="scss" scoped>
.vue-admin-better-wrapper {
position: relative;
width: 100%;
height: 100%;
[class*='vab-layout-'] {
:deep() {
.vab-layout-header {
box-shadow: $base-box-shadow;
}
}
&.fixed {
padding-top: $base-nav-height + $base-tabs-height;
}
&.fixed.no-tabs-bar {
padding-top: $base-nav-height;
}
}
:deep() {
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: $base-z-index - 1;
width: 100%;
}
.vab-main {
position: relative;
width: auto;
min-height: 100%;
margin-left: var(--el-left-menu-width);
&.is-collapse-main {
margin-left: $base-left-menu-width-min;
.fixed-header {
width: $base-right-content-width-min;
}
}
&:not(.is-collapse-main) {
.fixed-header {
width: calc(100% - var(--el-left-menu-width));
}
}
}
}
/* 手机端开始 */
&.mobile {
:deep() {
.vab-layout-vertical {
.el-scrollbar.vab-side-bar.is-collapse {
width: 0;
}
.vab-main {
.fixed-header {
width: 100%;
}
margin-left: 0;
}
}
/* 隐藏分页和页码跳转 */
.el-pager,
.el-pagination__jump {
display: none;
}
}
}
/* 手机端结束 */
}
</style>

View File

@@ -0,0 +1,19 @@
import type { App, DirectiveBinding } from 'vue'
import { hasPermission } from '@/utils/permission'
export default {
install(app: App<Element>) {
/**
* @description 自定义指令v-permissions
*/
app.directive('permissions', {
mounted(el: any, binding: DirectiveBinding) {
const { value } = binding
if (value)
if (!hasPermission(value))
el.parentNode && el.parentNode.removeChild(el)
},
})
},
}

View File

@@ -0,0 +1,30 @@
import { App } from 'vue'
import pinia from '@/store'
import { useErrorLogStore } from '@/store/modules/errorLog'
import { errorLog } from '@/config'
import { isArray, isString } from '@/utils/validate'
export const needErrorLog = () => {
const errorLogArray = isArray(errorLog)
? [...errorLog]
: isString(errorLog)
? [...[errorLog]]
: []
return errorLogArray.includes(process.env.NODE_ENV as string)
}
export const addErrorLog = (err: any) => {
// eslint-disable-next-line no-console
if (!err.isRequest) console.error('vue-admin-better错误拦截:', err)
const url = window.location.href
const { addErrorLog } = useErrorLogStore(pinia)
addErrorLog({ err, url })
}
export default {
install(app: App<Element>) {
if (needErrorLog()) {
app.config.errorHandler = addErrorLog
}
},
}

View File

@@ -0,0 +1,26 @@
import { App } from 'vue'
import pinia from '@/store'
import { useSettingsStore } from '@/store/modules/settings'
export default {
install(app: App<Element>) {
if (process.env.NODE_ENV !== 'development') {
const { title } = useSettingsStore(pinia)
// eslint-disable-next-line no-console
console.log(
` %c ${title} %c 基于admin-plus ${__APP_INFO__['version']} 构建 `,
'color: #fadfa3; background: #030307; padding:5px 0;',
'background: #fadfa3; padding:5px 0;'
)
}
if (process.env.NODE_ENV !== 'development') {
const str = '\u0076\u0061\u0062\u002d\u0069\u0063\u006f\u006e\u0073'
const key = decodeURI(str.replace(/\\u/g, '%u'))
if (!__APP_INFO__['dependencies'][key]) {
// eslint-disable-next-line
// @ts-ignore
app.config.globalProperties = null
}
}
},
}

172
library/plugins/vab.ts Normal file
View File

@@ -0,0 +1,172 @@
import { App } from 'vue'
import mitt from 'mitt'
import _ from 'lodash'
import { loadingText, messageDuration } from '@/config'
import { globalPropertiesType } from '/#/library'
export let gp: globalPropertiesType
export default {
install(app: App<Element>) {
gp = {
/**
* @description 全局加载层
* @param {number} index 自定义加载图标类名ID
* @param {string} text 显示在加载图标下方的加载文案
*/
$baseLoading: (index = undefined, text = loadingText) => {
return ElLoading.service({
lock: true,
text,
spinner: index ? `vab-loading-type${index}` : index,
background: 'hsla(0,0%,100%,.8)',
})
},
/**
* @description 全局Message
* @param {string} message 消息文字
* @param {'success'|'warning'|'info'|'error'} type 主题
* @param {string} customClass 自定义类名
* @param {boolean} dangerouslyUseHTMLString 是否将message属性作为HTML片段处理
*/
$baseMessage: (
message,
type = 'info',
customClass,
dangerouslyUseHTMLString
) => {
ElMessage({
message,
type,
customClass,
duration: messageDuration,
dangerouslyUseHTMLString,
showClose: true,
})
},
/**
* @description 全局Alert
* @param {string|VNode} content 消息正文内容
* @param {string} title 标题
* @param {function} callback 若不使用Promise,可以使用此参数指定MessageBox关闭后的回调
*/
$baseAlert: (content, title = '温馨提示', callback = undefined) => {
if (title && typeof title == 'function') {
callback = title
title = '温馨提示'
}
ElMessageBox.alert(content, title, {
confirmButtonText: '确定',
dangerouslyUseHTMLString: true, // 此处可能引起跨站攻击建议配置为false
callback: () => {
if (callback) callback()
},
}).then(() => {})
},
/**
* @description 全局Confirm
* @param {string|VNode} content 消息正文内容
* @param {string} title 标题
* @param {function} callback1 确认回调
* @param {function} callback2 关闭或取消回调
* @param {string} confirmButtonText 确定按钮的文本内容
* @param {string} cancelButtonText 取消按钮的自定义类名
*/
$baseConfirm: (
content,
title,
callback1,
callback2,
confirmButtonText = '确定',
cancelButtonText = '取消'
) => {
ElMessageBox.confirm(content, title || '温馨提示', {
confirmButtonText,
cancelButtonText,
closeOnClickModal: false,
type: 'warning',
lockScroll: false,
})
.then(() => {
if (callback1) {
callback1()
}
})
.catch(() => {
if (callback2) {
callback2()
}
})
},
/**
* @description 全局Notification
* @param {string} message 说明文字
* @param {string} title 标题
* @param {'success'|'warning'|'info'|'error'} type 主题样式,如果不在可选值内将被忽略
* @param {'top-right'|'top-left'|'bottom-right'|'bottom-left'} position 自定义弹出位置
* @param duration 显示时间,毫秒
*/
$baseNotify: (
message,
title,
type = 'success',
position = 'top-right',
duration = messageDuration
) => {
ElNotification({
title,
message,
type,
duration,
position,
})
},
/**
* @description 表格高度
* @param {*} formType
*/
$baseTableHeight: (formType) => {
let height = window.innerHeight
const paddingHeight = 291
const formHeight = 60
if ('number' === typeof formType) {
height = height - paddingHeight - formHeight * formType
} else {
height = height - paddingHeight
}
return height
},
$pub: (...args: any[]) => {
_emitter.emit(_.head(args), args[1])
},
$sub: function () {
// eslint-disable-next-line prefer-rest-params
Reflect.apply(_emitter.on, _emitter, _.toArray(arguments))
},
$unsub: function () {
// eslint-disable-next-line prefer-rest-params
Reflect.apply(_emitter.off, _emitter, _.toArray(arguments))
},
}
const _emitter = mitt()
Object.keys(gp).forEach((key) => {
app.provide(key, gp[key as keyof typeof gp])
// 允许vue3下继续使用vue2中的this调用vab方法
app.config.globalProperties[key] = gp[key as keyof typeof gp]
})
if (process.env['NODE_' + 'ENV'] !== `${'deve' + 'lopme' + 'nt'}`) {
const key = 'vab-' + 'icons'
if (!__APP_INFO__['dependencies'][key]) {
// @ts-ignore
app.config.globalProperties = null
}
if (!process.env['VUE_' + 'APP_' + 'SECRET_' + 'KEY']) {
// @ts-ignore
app.config.globalProperties = null
}
}
},
}

12
library/shims-vab.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare let __APP_INFO__: any
// CSS
type CSSModuleClasses = { readonly [key: string]: string }
declare module '*.module.scss' {
const classes: CSSModuleClasses
export default classes
}
declare module '*.scss' {
const css: string
export default css
}

View File

@@ -0,0 +1,251 @@
/**
* @description 黑
*/
body.vab-theme-black {
$base-menu-background: #282c34;
@mixin container {
color: var(--el-color-white) !important;
background: $base-menu-background !important;
}
@mixin active {
&:hover {
color: var(--el-color-white) !important;
background-color: var(--el-color-primary) !important;
}
&.is-active {
color: var(--el-color-white) !important;
background-color: var(--el-color-primary) !important;
}
}
.logo-container-vertical,
.logo-container-horizontal,
.logo-container-comprehensive,
.logo-container-float {
@include container;
}
.logo-container-column {
.logo {
@include container;
}
}
.vab-column-bar-container.el-scrollbar {
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav {
@include container;
}
.el-tabs__item.is-active {
background: var(--el-color-primary) !important;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
}
.vab-column-bar-container-card.el-scrollbar {
.el-tabs {
.el-tabs__item.is-active {
background: transparent !important;
.vab-column-grid {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-column-bar-container-arrow.el-scrollbar {
.el-tabs {
.el-tabs__item.is-active {
background: transparent !important;
.vab-column-grid {
background: transparent !important;
}
}
}
}
.vab-layout-float,
.vab-layout-common,
.vab-layout-vertical,
.vab-layout-horizontal,
.vab-layout-comprehensive {
.el-menu {
@include container;
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include container;
}
}
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu-item {
@include active;
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
}
.vab-header {
@include container;
.vab-main {
@include container;
.right-panel {
.el-menu {
&--horizontal {
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include active;
}
}
}
[role='menubar'].el-menu--horizontal {
> .el-sub-menu.is-active[tabindex='0'] {
> .el-sub-menu__title {
color: var(--el-color-white) !important;
background-color: var(
--el-color-primary
) !important;
}
}
}
}
}
}
.vab-tabs {
&-more {
&-active,
&:hover {
.vab-tabs-more-icon {
.box:before,
.box:after {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-card {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border: 1px solid var(--el-color-primary) !important;
}
&:hover {
border: 1px solid var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-smart {
.el-tabs__header {
.el-tabs__item {
&.is-active {
background: var(--el-color-primary-light-9) !important;
}
&:after {
background-color: var(--el-color-primary) !important;
}
&:hover {
background: var(--el-color-primary-light-9) !important;
}
}
}
}
.vab-tabs-content-smooth {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
&:hover {
color: var(--el-color-primary) !important;
background: var(
--el-color-primary-light-9
) !important;
}
}
&:hover {
color: var(--el-color-black) !important;
}
}
}
}
}
.vab-nav {
.el-tabs__item.is-active,
.el-tabs__item:hover {
color: var(--el-color-primary) !important;
}
.el-tabs__active-bar {
background-color: var(--el-color-primary) !important;
}
}
#nprogress {
.bar {
background: var(--el-color-primary) !important;
}
.peg {
box-shadow:
0 0 10px var(--el-color-primary),
0 0 5px var(--el-color-primary) !important;
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* @description 菜单背景
*/
body.vab-background > #app {
$base-menu-background: url('~@/assets/theme_images/background-1.png')
no-repeat;
@mixin container {
color: $base-color-white !important;
background: $base-menu-background !important;
background-size: auto 100% !important;
}
@mixin transparent {
color: $base-color-white !important;
background: transparent !important;
}
@mixin active {
span {
color: $base-color-white !important;
}
&:hover {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
&.is-active {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
.vab-side-bar:not(.is-collapse),
.comprehensive-bar-container {
@include container;
.el-menu {
@include transparent;
.el-menu-item,
.el-sub-menu__title {
@include transparent;
@include active;
i,
svg {
@include transparent;
}
}
}
.logo-container-vertical,
.logo-container-comprehensive,
.logo-container-float {
@include transparent;
.logo .vab-icon,
.title {
@include transparent;
}
}
}
.vab-column-bar-container {
&.el-scrollbar {
.logo-container-column {
.logo {
@include container;
background: #034291 !important;
.vab-icon {
@include transparent;
}
}
}
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav,
.el-tabs__item {
@include transparent;
&.is-active {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
}
&.vab-column-bar-container-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
.vab-column-grid {
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,181 @@
/**
* @description 渐变
*/
body.vab-theme-ocean {
$base-color-blue: #1890ff;
$base-color-blue-active: #399efd;
@mixin container {
background: linear-gradient(to right, #006cff, #399efd) !important;
}
@mixin active {
&:hover {
color: $base-color-white;
background-color: $base-color-blue-active !important;
}
&.is-active {
color: $base-color-white;
background-color: $base-color-blue-active !important;
}
}
.logo-container-horizontal {
background: var(--el-color-primary) !important;
}
.logo-container-vertical,
.logo-container-comprehensive,
.logo-container-float {
@include container;
}
.logo-container-column {
.logo {
@include container;
}
}
.vab-column-bar-container {
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav {
@include container;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
&-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
}
}
}
}
}
.vab-layout-horizontal {
.vab-header {
background: var(--el-color-primary) !important;
}
.el-menu {
background: var(--el-color-primary) !important;
.el-sub-menu__title {
background: var(--el-color-primary) !important;
}
.el-menu-item {
background: var(--el-color-primary) !important;
}
}
.vab-side-bar,
.comprehensive-bar-container {
background: var(--el-color-primary) !important;
.el-menu-item {
@include active;
}
}
}
.vab-layout-vertical,
.vab-layout-comprehensive,
.vab-layout-common,
.vab-layout-float {
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu {
@include container;
@include active;
.el-sub-menu__title,
.el-menu-item {
background-color: transparent !important;
@include active;
&.is-active {
background-color: $base-color-blue-active !important;
}
}
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
}
.vab-header {
background-color: var(--el-color-primary) !important;
.vab-main {
.el-menu.el-menu {
background-color: var(--el-color-primary) !important;
&--horizontal {
.el-sub-menu,
.el-menu-item {
background-color: var(--el-color-primary) !important;
&.is-active {
color: $base-color-white !important;
background-color: $base-color-blue-active !important;
}
}
> .el-menu-item,
.el-sub-menu__title,
> .el-menu-item:hover,
> .el-sub-menu__title:hover {
color: $base-color-white !important;
background-color: var(--el-color-primary) !important;
i {
color: $base-color-white !important;
}
&.is-active {
color: $base-color-white !important;
background-color: $base-color-blue-active !important;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,293 @@
/**
* @description 白
*/
body.vab-theme-white {
$base-menu-background: #fff;
@mixin container {
color: #515a6e !important;
background: $base-menu-background !important;
}
@mixin active {
&:hover {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
i,
svg,
span[title] {
color: var(--el-color-primary) !important;
}
}
&.is-active {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
i,
svg,
span[title] {
color: var(--el-color-primary) !important;
}
}
}
.logo-container-common,
.logo-container-vertical,
.logo-container-horizontal,
.logo-container-comprehensive,
.logo-container-float {
@include container;
.title,
.vab-icon {
@include container;
}
}
.logo-container-column {
@include container;
.title {
@include container;
}
.logo,
.vab-icon {
@include container;
}
}
.vab-column-bar-container {
.el-tabs {
@include container;
.el-tabs__nav-wrap.is-left {
background: #f7faff !important;
}
.el-tabs__item,
.el-tabs__nav {
@include container;
}
.el-tabs__item.is-active {
color: var(--el-color-white) !important;
background: var(--el-color-primary) !important;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
&-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
}
}
}
}
&-arrow {
.el-tabs {
.el-tabs__item {
&.is-active {
color: var(--el-color-black) !important;
background: transparent !important;
.vab-column-grid {
background: transparent !important;
}
}
}
}
}
}
.vab-layout-float,
.vab-layout-common,
.vab-layout-vertical,
.vab-layout-horizontal,
.vab-layout-comprehensive {
.el-menu {
@include container;
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include container;
}
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu-item {
@include active;
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
}
.vab-header {
@include container;
.vab-main {
@include container;
.right-panel {
.user-name,
.user-name *,
> i,
> div > i,
> span > i,
> div > span > i,
> svg,
> div > svg,
> span > svg,
> div > span > svg,
.ri-notification-line,
.ri-translate,
.ri-bug-line {
@include container;
}
.el-menu {
&--horizontal {
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include active;
}
.el-sub-menu,
.el-menu-item {
&.is-active {
@include active;
}
}
> .el-sub-menu.is-active {
> .el-sub-menu__title {
background-color: var(
--el-color-primary-light-9
) !important;
@include active;
}
}
}
}
}
}
}
.vab-tabs {
&-more {
&-active,
&:hover {
.vab-tabs-more-icon {
.box:before,
.box:after {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-card {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border: 1px solid var(--el-color-primary) !important;
}
&:hover {
border: 1px solid var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-smart {
.el-tabs__header {
.el-tabs__item {
&.is-active {
background: var(--el-color-primary-light-9) !important;
}
&:after {
background-color: var(--el-color-primary) !important;
}
&:hover {
background: var(--el-color-primary-light-9) !important;
}
}
}
}
.vab-tabs-content-smooth {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
&:hover {
color: var(--el-color-primary) !important;
background: var(
--el-color-primary-light-9
) !important;
}
}
&:hover {
color: var(--el-color-black) !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
.dots-loader:not(:required) {
position: relative;
display: inline-block;
width: 7px;
height: 7px;
margin-bottom: 30px;
overflow: hidden;
text-indent: -9999px;
background: transparent;
border-radius: 100%;
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
transform-origin: 50% 50%;
animation: dots-loader 5s infinite ease-in-out;
}
@keyframes dots-loader {
0% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
8.33% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
16.67% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 14px 14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
25% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
33.33% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae -14px -14px 0 7px;
}
41.67% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
50% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
58.33% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
66.67% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 -14px -14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
75% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
83.33% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae 14px 14px 0 7px;
}
91.67% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
100% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
}

View File

@@ -0,0 +1,104 @@
.gauge-loader:not(:required) {
position: relative;
display: inline-block;
width: 64px;
height: 32px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #6ca;
border-top-left-radius: 32px;
border-top-right-radius: 32px;
}
.gauge-loader:not(:required)::before {
position: absolute;
top: 5px;
left: 30px;
width: 4px;
height: 27px;
content: '';
background: white;
border-radius: 2px;
transform-origin: 50% 100%;
animation: gauge-loader 4000ms infinite ease;
}
.gauge-loader:not(:required)::after {
position: absolute;
top: 26px;
left: 26px;
width: 13px;
height: 13px;
content: '';
background: white;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
}
@keyframes gauge-loader {
0% {
transform: rotate(-50deg);
}
10% {
transform: rotate(20deg);
}
20% {
transform: rotate(60deg);
}
24% {
transform: rotate(60deg);
}
40% {
transform: rotate(-20deg);
}
54% {
transform: rotate(70deg);
}
56% {
transform: rotate(78deg);
}
58% {
transform: rotate(73deg);
}
60% {
transform: rotate(75deg);
}
62% {
transform: rotate(70deg);
}
70% {
transform: rotate(-20deg);
}
80% {
transform: rotate(20deg);
}
83% {
transform: rotate(25deg);
}
86% {
transform: rotate(20deg);
}
89% {
transform: rotate(25deg);
}
100% {
transform: rotate(-50deg);
}
}

View File

@@ -0,0 +1,51 @@
.inner-circles-loader:not(:required) {
position: relative;
display: inline-block;
width: 50px;
height: 50px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: rgba(25, 165, 152, 0.5);
border-radius: 50%;
transform: translate3d(0, 0, 0);
}
.inner-circles-loader:not(:required)::before,
.inner-circles-loader:not(:required)::after {
position: absolute;
top: 0;
display: inline-block;
width: 50px;
height: 50px;
content: '';
border-radius: 50%;
}
.inner-circles-loader:not(:required)::before {
left: 0;
background: #c7efcf;
transform-origin: 0 50%;
animation: inner-circles-loader 3s infinite;
}
.inner-circles-loader:not(:required)::after {
right: 0;
background: #eef5db;
transform-origin: 100% 50%;
animation: inner-circles-loader 3s 0.2s reverse infinite;
}
@keyframes inner-circles-loader {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(360deg);
}
100% {
transform: rotate(0deg);
}
}

View File

@@ -0,0 +1,341 @@
.plus-loader:not(:required) {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #f86;
-moz-border-radius: 24px;
-webkit-border-radius: 24px;
border-radius: 24px;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
-moz-animation: plus-loader-background 3s infinite ease-in-out;
-webkit-animation: plus-loader-background 3s infinite ease-in-out;
animation: plus-loader-background 3s infinite ease-in-out;
}
.plus-loader:not(:required)::after {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #f86;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-top 3s infinite linear;
-webkit-animation: plus-loader-top 3s infinite linear;
animation: plus-loader-top 3s infinite linear;
}
.plus-loader:not(:required)::before {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #fc6;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-bottom 3s infinite linear;
-webkit-animation: plus-loader-bottom 3s infinite linear;
animation: plus-loader-bottom 3s infinite linear;
}
@keyframes plus-loader-top {
2.5% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
13.75% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
13.76% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
25% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
27.5% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
41.25% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
41.26% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
50% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
}
52.5% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
63.75% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
63.76% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
75% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
77.5% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
91.25% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
91.26% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-bottom {
0% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
50% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
75% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
100% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-background {
0% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
}
25% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
27.5% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
50% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
52.5% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
}
75% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
77.5% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
}
100% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}

377
library/styles/normalize.scss vendored Normal file
View File

@@ -0,0 +1,377 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline dotted;
/* 2 */
border-bottom: none;
/* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0;
/* 2 */
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
color: inherit;
/* 2 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
/* 1 */
font: inherit;
-webkit-appearance: button;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -0,0 +1,39 @@
/**
* @description vue过渡动画
*/
.fade-transform {
&-leave-active,
&-enter-active {
transition: $base-transition;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
.no-transform {
&-leave-active,
&-enter-active {
transition: none;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
/**
* @description 旋转动画
*/
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}

633
library/styles/vab.scss Normal file
View File

@@ -0,0 +1,633 @@
/**
* @description 全局样式
*/
@import 'element-plus/theme-chalk/display.css';
@import './normalize';
@import './transition';
@mixin base-scrollbar {
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: mix($base-color-white, $base-menu-background, 90%);
border: 3px solid transparent;
border-radius: 7px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: mix($base-color-white, $base-menu-background, 80%);
}
}
.vab-layout-header,
[class*='-bar-container'] {
transition: $base-transition;
* {
transition: $base-transition;
}
}
html {
body,
body[class*='vab-theme-'] {
position: relative;
box-sizing: border-box;
height: 100vh;
padding: 0;
overflow: hidden;
font-family: 'PingFang SC', Arial, 'Microsoft YaHei', sans-serif;
font-size: $base-font-size-default;
color: var(--el-color-black);
background: $base-color-background;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
#app {
height: 100vh;
overflow: auto;
@include base-scrollbar;
.vab-main {
transition: $base-transition;
.vab-app-main {
width: 100%;
padding: $base-padding;
overflow: hidden;
transition: $base-transition;
>section {
background: var(--el-color-white);
transition: $base-transition;
>[class*='-container'] {
min-height: $base-keep-alive-height;
padding: $base-padding;
background: var(--el-color-white);
transition: $base-transition;
}
}
}
}
}
* {
box-sizing: border-box;
outline: none !important;
@include base-scrollbar;
}
[class*='ri-'] {
vertical-align: -3px !important;
}
.vab-icon {
margin: 0 3px 0 0 !important;
}
/*a标签 */
a {
color: var(--el-color-primary);
text-decoration: none;
}
/*图片 */
img {
object-fit: cover;
&[src=''],
&:not([src]) {
opacity: 0;
}
}
/* vab-fullscreen全屏 */
.vab-fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
z-index: $base-z-index + 3 !important;
box-sizing: border-box !important;
width: 100vw !important;
height: 100vh !important;
padding-bottom: 15px !important;
overflow: auto !important;
}
/* vab-dropdown下拉动画 */
.vab-dropdown {
transition: $base-transition;
&-active {
transform: rotateZ(180deg);
}
}
/* vab-dot圆点动画 */
.vab-dot {
position: relative;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 3px;
vertical-align: middle;
border-radius: 50%;
span {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
animation: vabDot 1.2s ease-in-out infinite;
@keyframes vabDot {
0% {
opacity: 0.6;
transform: scale(0.8);
}
to {
opacity: 0;
transform: scale(2.4);
}
}
}
&-success {
background: var(--el-color-success);
span {
background: var(--el-color-success);
}
}
&-error {
background: var(--el-color-error);
span {
background: var(--el-color-error);
}
}
}
/* vab-data-empty占位图 */
.vab-data-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 600px;
margin: auto;
}
/* el-descriptions */
.el-descriptions {
&__title {
padding-left: 10px;
border-left: 5px solid var(--el-color-primary);
}
}
/* el-button按钮 */
.el-button {
border-radius: var(--el-border-radius-base);
&:hover,
&:focus,
&:active,
&.is-disabled {
background-clip: padding-box;
}
&.is-round {
border-radius: var(--el-border-radius-round);
}
&.is-circle {
border-radius: var(--el-border-radius-circle);
}
[class*='el-icon-']+span,
span+[class*='el-icon-'],
[class*='ri-']+span,
span+[class*='ri-'] {
margin-left: 3px;
}
}
/* el-tag */
.el-tag {
border-radius: var(--el-border-radius-base);
&+.el-tag {
margin-left: 10px;
}
&--light:not(&--success, &--info, &--warning, &--danger) {
--el-tag-bg-color: var(--el-color-primary-light-9);
--el-tag-border-color: var(--el-color-primary-light-8);
--el-tag-text-color: var(--el-color-primary);
--el-tag-hover-color: var(--el-color-primary);
}
&--dark:not(&--success, &--info, &--warning, &--danger) {
--el-tag-bg-color: var(--el-color-primary);
--el-tag-border-color: var(--el-color-primary);
--el-tag-hover-color: var(--el-color-primary-2);
}
&.is-round {
border-radius: var(--el-border-radius-round);
}
}
/* .el-select-tags */
.el-select-tags-wrapper {
.el-tag.el-tag {
margin-left: 0px;
}
}
/* el-select */
.el-select {
min-width: 115px;
}
a+a,
/* span + span, */
a+.el-button,
.el-button+a {
margin-left: 10px;
}
.el-drawer__wrapper {
outline: none !important;
* {
outline: none !important;
}
}
/* el-overlay遮罩 */
.el-overlay {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
}
/* el-image-viewer遮罩 */
.el-image-viewer__mask {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
}
/* v-modal遮罩 */
.v-modal {
z-index: $base-z-index;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0.6;
//backdrop-filter: blur(10px);
}
/* el-loading-mask遮罩 */
.el-loading-mask {
z-index: $base-z-index - 10 !important;
&.is-fullscreen {
z-index: $base-z-index + 99 !important;
}
}
/* el-scrollbar滚动条 */
.el-scrollbar {
height: 100%;
&__bar {
z-index: 999;
}
&__thumb {
background-color: mix($base-color-white,
$base-menu-background,
90%);
&:hover {
background-color: mix($base-color-white,
$base-menu-background,
80%);
}
}
}
/* el-form表单 */
.el-form--label-top {
.el-form-item__label {
padding: 0;
}
}
.el-form-item__label {
padding: 0 10px 0 0;
}
.el-range-editor--small {
.el-range__icon,
.el-range__close-icon {
line-height: 23.5px;
}
}
/* el-badge */
.el-badge__content {
border: 0;
}
/* .el-page-header */
.el-page-header {
margin: 0 0 $base-margin 0;
}
/* el-alert */
.el-alert {
margin: 0 0 $base-margin 0;
&__closebtn {
position: absolute !important;
}
&--success.is-light {
color: var(--el-color-success);
background-color: var(--el-color-success-lighter);
border: 1px solid var(--el-color-success);
i {
color: var(--el-color-success);
}
}
&--info.is-light {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary);
i {
color: var(--el-color-primary);
}
}
&--warning.is-light {
color: var(--el-color-warning);
background-color: var(--el-color-warning-lighter);
border: 1px solid var(--el-color-warning);
i {
color: var(--el-color-warning);
}
}
&--error.is-light {
color: var(--el-color-error);
background-color: var(--el-color-error-lighter);
border: 1px solid var(--el-color-error);
i {
color: var(--el-color-error);
}
}
}
/* el-divider间隔线 */
.el-divider--horizontal {
margin: 8px 0 $base-margin + 8px 0;
.el-divider__text {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
}
/* nprogress进度条 */
#nprogress {
position: fixed;
z-index: $base-z-index + 3;
.bar {
background: var(--el-color-primary);
}
.peg {
box-shadow:
0 0 10px var(--el-color-primary),
0 0 5px var(--el-color-primary);
}
}
/* el-table表格 */
.el-table {
.el-table__body-wrapper {
@include base-scrollbar;
}
th {
background: #f5f7fa !important;
}
td,
th {
position: relative;
box-sizing: border-box;
.cell {
font-size: $base-font-size-default;
font-weight: normal;
color: #606266;
.el-image {
width: 50px;
height: 50px;
border-radius: $base-border-radius;
}
}
}
}
/* el-pagination分页 */
.el-pagination {
justify-content: center;
margin: $base-margin 0 0 0;
font-weight: normal;
color: var(--el-color-black);
}
/* el-menu菜单开始 */
.el-menu,
.vab-column-grid {
user-select: none;
/* plus处理图标间距 */
div,
li,
span {
i+span {
margin-left: 3px;
}
}
&.vab-column-grid-card,
&.vab-column-grid-vertical {
div {
i+span {
margin-left: 0;
}
}
}
}
.el-dialog__body {
padding-right: 30px;
}
/* el-dialog、el-message-box、el-popover、el-button、el-tag */
@media (max-width: 576px) {
.el-dialog,
.el-message-box,
.el-popover.el-popper {
width: 95% !important;
}
.el-button {
margin-bottom: 10px;
}
}
/* el-card卡片 */
.el-card {
margin-bottom: $base-margin;
border-radius: var(--el-border-radius-base);
&__header {
position: relative;
.card-header-tag {
position: absolute;
top: 15px;
right: 20px;
}
}
&__body {
padding: $base-padding;
}
}
/* .vab-hey-message */
.vab-hey-message {
@mixin vab-hey-message {
padding: 15px;
background-color: var(--el-color-white);
border-color: var(--el-color-white);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
.el-message__content {
padding-right: $base-padding;
color: #34495e;
}
.el-icon-close {
color: #34495e;
&:hover {
opacity: 0.8;
}
}
}
&-info {
@include vab-hey-message;
i {
color: $base-color-grey;
}
}
&-success {
@include vab-hey-message;
i {
color: var(--el-color-success);
}
}
&-warning {
@include vab-hey-message;
i {
color: var(--el-color-warning);
}
}
&-error {
@include vab-hey-message;
i {
color: var(--el-color-error);
}
}
}
/* vab-table-expand */
.vab-table-expand {
padding: $base-padding;
line-height: 30px;
&-title {
display: inline-block;
width: 80px;
font-weight: bold;
}
}
:not(.no-background-container).auto-height-container {
display: flex;
flex-direction: column;
height: var(--el-container-height);
.el-table {
flex: 1;
}
.el-scrollbar {
//margin-right: -20px;
.vab-auto-box {
flex: 1;
width: 100%;
padding: 0 var(--el-padding) 0 0;
}
}
@media (max-width: 1024px) {
height: auto;
}
}
.el-card__body {
overflow: hidden;
flex: none;
}
}
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #1890ff;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #77e19d;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #41b584;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #1890ff;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #6954f0;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #1890ff;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #f34d37;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #ffa194;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,109 @@
/**
* @description 全局主题变量配置
*/
//颜色配置
$base-color-white: #ffffff;
$base-color-black: #515a6e;
$base-color-primary: #1890ff;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff6700;
$base-color-error: #ff4d4f;
$base-color-grey: rgba(0, 0, 0, 0.65);
$base-color-background: #f6f8f9;
$base-color-text-primary: #303133;
$base-color-text-regular: #606266;
$base-color-text-secondary: #909399;
$base-color-text-placeholder: #c0c4cc;
$base-border-color-base: #dcdfe6;
$base-border-color-light: #e4e7ed;
$base-border-color-lighter: #ebeef5;
$base-border-color-extra-light: #f2f6fc;
$base-background-color-base: #f5f7fa;
//默认层级
$base-z-index: 1999;
//分栏最左侧菜单背景色
$base-column-first-menu-background: #282c34;
//分栏菜单背景色
$base-column-second-menu-background: #fff;
//分栏菜单选中背景色
$base-column-second-menu-active: mix($base-color-white,
$base-color-primary,
10%);
//横向、纵向菜单背景色
$base-menu-background: #282c34;
//菜单文字颜色
$base-menu-color: hsla(0, 0%, 100%, 0.95);
//菜单选中文字颜色
$base-menu-color-active: hsla(0, 0%, 100%, 0.95);
//菜单选中背景色
$base-menu-active: $base-color-primary;
//标题颜色
$base-title-color: #fff;
//字体大小配置
$base-font-size-small: 12px;
$base-font-size-default: 14px;
$base-font-size-big: 16px;
$base-font-size-bigger: 18px;
$base-font-size-max: 22px;
//最大宽度
$base-main-width: 1279px;
//圆角
$base-border-radius: 5px;
//边框颜色
$base-border-color: #dcdfe6;
//输入框高度
$base-input-height: 32px;
//默认margin
$base-margin: 20px;
//默认padding
$base-padding: 20px;
//默认阴影
$base-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
//横向top-bar、logo、一级菜单的高度
$base-header-height: 60px;
//纵向、综合、分栏logo的高度
$base-logo-height: 60px;
//顶部nav-bar的高度
$base-nav-height: 60px;
//顶部标签页tabs-bar的高度
$base-tabs-height: 50px;
//顶部标签页tabs-bar中每一个item的高度
$base-tag-item-height: 34px;
//菜单li标签的高度
$base-menu-item-height: 50px;
//app-main的高度
$base-keep-alive-height: calc(100vh - #{$base-nav-height} - #{$base-tabs-height} - #{$base-padding} * 2 - 55px);
//纵向左侧导航未折叠的宽度
$base-left-menu-width: 266px;
//纵向左侧导航已折叠的宽度
$base-left-menu-width-min: 64px;
//纵向左侧导航已折叠右侧内容的宽度
$base-right-content-width-min: calc(100% - #{$base-left-menu-width-min});
//默认动画
$base-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
border 0s,
color 0.1s,
font-size 0s;
:export {
// 菜单文字颜色变量导出
menu-color: $base-menu-color;
// 菜单选中文字颜色变量导出
menu-color-active: $base-menu-color-active;
// 菜单背景色变量导出
menu-background: $base-menu-background;
// 分栏菜单背景色变量导出
column-second-menu-background: $base-column-second-menu-background;
// 导出圆角
vab-border-radius: $base-border-radius;
}
:root {
--el-container-height: #{$base-keep-alive-height};
--el-margin: #{$base-margin};
--el-padding: #{$base-padding};
--el-border-radius-base: #{$base-border-radius} !important;
}