chore(frontend): 落地 ADS 治理体系并收录前端底座基线代码
@@ -0,0 +1,24 @@
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ElConfigProvider,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
locale: zhCn,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 735 B |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 803 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 437 B |
|
After Width: | Height: | Size: 537 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,297 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
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 createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
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 eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
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 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 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 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 shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
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 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 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 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 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 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')
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import request from '@/framework/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { UserInfo } from './types';
|
||||
|
||||
function replaceAcasService(url){
|
||||
if(publicData.replaceAcasService){
|
||||
return url.replace('/acas/','/'+publicData.replaceAcasService+'/')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取路由列表
|
||||
*/
|
||||
export function listRoutes(params: any = {}) {
|
||||
return request({
|
||||
url: replaceAcasService('/acas/user/allowAccessMenuAndModules'),
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**获取key */
|
||||
export function getKey(params: any = {}) {
|
||||
return request({
|
||||
url:replaceAcasService('/acas/pk'),
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**刷新*/
|
||||
export function baseRefresh(url:string,data: any = {}) {
|
||||
return request({
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
url:replaceAcasService(url),
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**退出登录 */
|
||||
export function baseLogout(url:string,data: any = {}) {
|
||||
return request({
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
url:replaceAcasService(url),
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**登录后修改密码 */
|
||||
export function baseLoginUpdatePwd(url:string,data: any = {}) {
|
||||
return request({
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
url:replaceAcasService(url),
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**获取key */
|
||||
export function getEmployee(params: any = {}) {
|
||||
return request({
|
||||
url:replaceAcasService('/acas/employee/getEmployee'),
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**获取员工管理业务域*/
|
||||
export function getBusinessDomain(params: any = {}) {
|
||||
return request({
|
||||
url: replaceAcasService('/acas/user/loginUserdomain'),
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**获取有权限的子系统*/
|
||||
export function getAllowAccessSubsystem(params: any = {}) {
|
||||
return request({
|
||||
url: replaceAcasService('/acas/user/allowAccessSubsystem'),
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**获取员工信息 */
|
||||
export function getUsermessage(params: any = {}) {
|
||||
if (import.meta.env.MODE === 'development'){
|
||||
return new Promise((resolve) => {
|
||||
resolve({"user":{"tenantId":"0000","tenantName":"奥博克","userId":"0","userName":"系统管理员","deptId":"01","deptName":"开发部","avatarUrl":""}});
|
||||
});
|
||||
}else{
|
||||
return request({
|
||||
url:'/acas/user/message',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 登录用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
dept: any;
|
||||
employee: any;
|
||||
roles: any;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import request from '@/framework/utils/request';
|
||||
|
||||
/**获取异步事件列表 */
|
||||
export function queryAsyncEvents(params: any = {}) {
|
||||
return request({
|
||||
url:'/sms/asyncEvent/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-breadcrumb class="h-[50px] flex items-center">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
|
||||
<span
|
||||
v-if="
|
||||
item.redirect === 'noredirect' || index === breadcrumbs.length - 1
|
||||
"
|
||||
class="text-[var(--el-disabled-text-color)]"
|
||||
>{{ item.meta.title }}</span
|
||||
>
|
||||
<a v-else @click.prevent="handleLink(item)">
|
||||
{{ item.meta.title }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref, watch } from "vue";
|
||||
import { useRoute, RouteLocationMatched } from "vue-router";
|
||||
import { compile } from "path-to-regexp";
|
||||
import router from "@/framework/router";
|
||||
// import { translateRouteTitleI18n } from "@/framework/utils/i18n";
|
||||
|
||||
const currentRoute = useRoute();
|
||||
const pathCompile = (path: string) => {
|
||||
const { params } = currentRoute;
|
||||
const toPath = compile(path);
|
||||
return toPath(params);
|
||||
};
|
||||
|
||||
const breadcrumbs = ref([] as Array<RouteLocationMatched>);
|
||||
|
||||
function getBreadcrumb() {
|
||||
console.log(currentRoute.matched,8888);
|
||||
|
||||
let matched = currentRoute.matched.filter(
|
||||
(item) => item.meta && item.meta.title
|
||||
);
|
||||
// const first = matched[0];
|
||||
// if (!isDashboard(first)) {
|
||||
// matched = [
|
||||
// { path: "/home", meta: { title: "首页" } } as any,
|
||||
// ].concat(matched);
|
||||
// }
|
||||
breadcrumbs.value = matched.filter((item) => {
|
||||
return item.meta && item.meta.title && item.meta.breadcrumb !== false;
|
||||
});
|
||||
}
|
||||
|
||||
function isDashboard(route: RouteLocationMatched) {
|
||||
const name = route && route.name;
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
name.toString().trim().toLocaleLowerCase() ===
|
||||
"首页"
|
||||
);
|
||||
}
|
||||
|
||||
function handleLink(item: any) {
|
||||
const { redirect, path } = item;
|
||||
if (redirect) {
|
||||
router.push(redirect).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
router.push(pathCompile(path)).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.path,
|
||||
(path) => {
|
||||
if (path.startsWith("/redirect/")) {
|
||||
return;
|
||||
}
|
||||
getBreadcrumb();
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
getBreadcrumb();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
// 覆盖 element-plus 的样式
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/img/403.png" class="not-img" alt="403" />
|
||||
<div class="not-detail">
|
||||
<h2>403</h2>
|
||||
<h4>抱歉,您无权访问该页面~🙅♂️🙅♀️</h4>
|
||||
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="403">
|
||||
// import { HOME_URL } from "@/config";
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/img/404.png" class="not-img" alt="404" />
|
||||
<div class="not-detail">
|
||||
<h2>404</h2>
|
||||
<h4>抱歉,您访问的页面不存在~</h4>
|
||||
<h4>有可能是您没有访问权限~</h4>
|
||||
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="404">
|
||||
// import { HOME_URL } from "@/config";
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/img/500.png" class="not-img" alt="500" />
|
||||
<div class="not-detail">
|
||||
<h2>500</h2>
|
||||
<h4>抱歉,您的网络不见了~🤦♂️🤦♀️</h4>
|
||||
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="500">
|
||||
// import { HOME_URL } from "@/config";
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
.not-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.not-img {
|
||||
margin-right: 120px;
|
||||
}
|
||||
.not-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h2,
|
||||
h4 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 60px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
h4 {
|
||||
margin: 30px 0 20px;
|
||||
font-size: 19px;
|
||||
font-weight: normal;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.el-button {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<!-- <div class="hambuger-wrapper" :class="{active: isActive}" @click="toggleClick">
|
||||
<el-icon size="20">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
</div> -->
|
||||
<div class="hambuger-wrapper" @click="toggleClick">
|
||||
<img :src="foldPng" style="width: 28px;height: 28px;" :style="{transform: isActive ? '' :'rotate(180deg)'}"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import foldPng from '@/assets/img/icon/fold.png'
|
||||
defineProps({
|
||||
isActive: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["toggleClick"]);
|
||||
|
||||
function toggleClick() {
|
||||
emit("toggleClick");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hambuger-wrapper{
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
background: rgba(0,0,0,.025);
|
||||
}
|
||||
}
|
||||
.active{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<img class="menu-img" :src="imgSrc"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { getAssetsImages } from '@/framework/utils/index'
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: String,
|
||||
default: "/menu",
|
||||
},
|
||||
imgName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "",
|
||||
}
|
||||
});
|
||||
|
||||
const imgSrc = computed(() => getAssetsImages(props.imgName, props.path));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-img{
|
||||
margin-left: 3px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="批量添加" :destroy-on-close="true" width="580px" draggable top="1vh">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="模板下载:">
|
||||
<el-button type="primary" @click="downloadFile">
|
||||
<el-icon color="#fff">
|
||||
<Download />
|
||||
</el-icon> 模板下载
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件上传:">
|
||||
<el-upload class="upload" drag :action="uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请按模板上传文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type {UploadRawFile } from 'element-plus'
|
||||
|
||||
let props = defineProps({
|
||||
templateName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
uploadUrl: String
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
// 下载模板
|
||||
const downloadFile = () => {
|
||||
if (props.templateName === '') {
|
||||
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
|
||||
}
|
||||
|
||||
let dom = document.createElement("a");
|
||||
dom.style.display = 'none'
|
||||
dom.target = '_blank'
|
||||
dom.href = `./templete/${props.templateName}`
|
||||
document.body.appendChild(dom);
|
||||
dom.click();
|
||||
document.body.removeChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 文件上传之前判断
|
||||
* @param file 上传的文件
|
||||
* */
|
||||
const beforeExcelUpload = (file: UploadRawFile) => {
|
||||
const suffix = file.name.split('.').pop()
|
||||
const isExcel = ['xls', 'xlsx'].includes(suffix);
|
||||
if (!isExcel)
|
||||
ElMessage({type: 'warning',message: '上传文件只能是 xls / xlsx 格式' })
|
||||
|
||||
// const fileSize = file.size
|
||||
// if (!fileSize)
|
||||
// setTimeout(() => {
|
||||
// ElNotification({
|
||||
// title: "温馨提示",
|
||||
// message: `上传文件大小不能超过 ${parameter.value.fileSize}MB!`,
|
||||
// type: "warning"
|
||||
// });
|
||||
// }, 0);
|
||||
return isExcel;
|
||||
};
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const excelUploadSuccess = (response: any) => {
|
||||
emit('success', response)
|
||||
}
|
||||
|
||||
// 上传错误提示
|
||||
const excelUploadError = () => {
|
||||
ElMessage({
|
||||
title: "温馨提示",
|
||||
message: `批量添加失败,请您重新上传!`,
|
||||
type: "error"
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
dialogVisible
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.upload {
|
||||
width: 80%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="" :class="'pagination ' + { hidden: hidden }">
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:background="background"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed, PropType } from "vue";
|
||||
import { scrollTo } from "@/framework/utils/scroll-to";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
required: true,
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: "total, sizes, prev, pager, next, jumper",
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["pagination", "update:page", "update:limit"]);
|
||||
|
||||
const currentPage = useVModel(props, "page", emit);
|
||||
|
||||
const pageSize = useVModel(props, "limit", emit);
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
emit("pagination", { page: currentPage, limit: val });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPage.value = val;
|
||||
emit("pagination", { page: val, limit: props.limit });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
padding: 20px 12px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<el-select v-model="value" :placeholder="placeholder" style="width: 100%;" clearable :filterable="filterable">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px;"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
/**
|
||||
* SearchSelect
|
||||
* @author nielang
|
||||
* @description 用于实时检索的下拉框
|
||||
* @property {String} placeholder input的placeholder
|
||||
* @property {Function} request 接口请求函数
|
||||
* @property {String} requestParam 接口请求参数名称
|
||||
* @property {Boolean} showOptionValue 选项中是否显示value属性
|
||||
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
|
||||
* @property {String} optionLabel 接口用于显示的属性
|
||||
* @property {Boolean} filterable 是否可搜索
|
||||
*/
|
||||
const props = defineProps({
|
||||
modelValue:{
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
request:{
|
||||
type: Function,
|
||||
require: true
|
||||
},
|
||||
requestParam: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
showOptionValue: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
optionValue: {
|
||||
type: [String, Number],
|
||||
default: "id",
|
||||
},
|
||||
optionLabel: {
|
||||
type: [String, Number],
|
||||
default: "name",
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
interface ListItem {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
const options = ref<ListItem[]>([])
|
||||
const initOptions = () => {
|
||||
if (props.request) {
|
||||
props.request(props.requestParam).then((data:any) => {
|
||||
options.value = data.map((item: any) => {
|
||||
return {
|
||||
value: item[props.optionValue],
|
||||
label: (props.showOptionValue ? `【${item[props.optionValue]}】` : '') + item[props.optionLabel]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable
|
||||
:remote-method="remoteMethod" :loading="loading" clearable >
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from 'vue'
|
||||
/**
|
||||
* SearchSelect
|
||||
* @author nielang
|
||||
* @description 用于实时检索的下拉框
|
||||
* @property {String} placeholder input的placeholder
|
||||
* @property {Function} request 接口请求函数
|
||||
* @property {String} requestParam 接口请求参数名称
|
||||
* @property {Boolean} showOptionValue 选项中是否显示value属性
|
||||
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
|
||||
* @property {String} optionLabel 接口用于显示的属性
|
||||
*/
|
||||
const props = defineProps({
|
||||
modelValue:{
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入商品编码/名称",
|
||||
},
|
||||
request:{
|
||||
type: Function,
|
||||
require: true
|
||||
},
|
||||
requestParam: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showOptionValue: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
optionValue: {
|
||||
type: [String, Number],
|
||||
default: "id",
|
||||
},
|
||||
optionLabel: {
|
||||
type: [String, Number],
|
||||
default: "name",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(false);
|
||||
interface ListItem {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
|
||||
const options = ref<ListItem[]>([])
|
||||
const remoteMethod = (query: string) => {
|
||||
if (query) {
|
||||
loading.value = true
|
||||
if (props.request) {
|
||||
const params:any = {}
|
||||
params[props.requestParam] = query
|
||||
props.request(params).then((data:any) => {
|
||||
loading.value = false
|
||||
options.value = data.map((item: any) => {
|
||||
return {
|
||||
value: item[props.optionValue],
|
||||
label: (props.showOptionValue ? `【${item[props.optionValue]}】` : '') + item[props.optionLabel]
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
options.value = []
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--主题-->
|
||||
<template>
|
||||
<div class="form-container ab-m-t-16 title">
|
||||
<el-page-header :icon="null" >
|
||||
<template #title>
|
||||
<div style="display:flex;align-items: center;">
|
||||
<slot name="titleBack"></slot>
|
||||
<span style="font-size: 25px;font-weight: 500;"><slot name="titleName"></slot></span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<span style="font-size: 15px;"><slot name="titleContent"></slot></span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot name="titleOperate"></slot>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title{
|
||||
padding: 10px 0px 10px 10px;;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- 内容分隔组件 -->
|
||||
<template>
|
||||
<div class="title-container" >
|
||||
<div class="header" :style="{ backgroundColor: bgColor, fontSize, color, borderBottom: arrow?'1px solid #CDD0D6':'' }" @click="handleShow" >
|
||||
<div class="left">
|
||||
<span class="line" :style="{display: showLine? 'inline' : 'none', borderColor: lineColor}"></span> <span class="name">{{ title }}</span>
|
||||
</div>
|
||||
<div class="right" :style="{display: arrow? 'inline' : 'none'}">
|
||||
<el-icon :style="{transform: show ? 'rotate(90deg)' : '', transition: 'transform 0.2s linear'}"><ArrowRightBold /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-show="show" :style="{height: show ? 'auto' : '0px'}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true, }, //标签名称
|
||||
bgColor: { type: String, default: '#fff' },//背景颜色
|
||||
color: { type: String, default: '#222' },//文字颜色
|
||||
fontSize: { type: String, default: '20px' },//字体大小
|
||||
showLine: { type: Boolean, default: true },//是否显示图标
|
||||
lineColor: { type: String, default: '' },//图标颜色
|
||||
arrow: { type: Boolean, default: false },//展开、收起功能
|
||||
})
|
||||
|
||||
const show = ref(true)
|
||||
const handleShow = () => {
|
||||
if (!props.arrow) {
|
||||
return
|
||||
}
|
||||
show.value = !show.value
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
// border-bottom: 1px solid #CDD0D6;
|
||||
|
||||
.left{
|
||||
.line {
|
||||
width: 2px;
|
||||
border: 2px solid var(--el-color-primary);
|
||||
border-radius: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.right{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.content{
|
||||
height: 0px;
|
||||
transition: height 2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- 点击复制 -->
|
||||
<template >
|
||||
<el-tooltip v-if="content" content="复制" placement="top" effect="dark">
|
||||
<el-icon class="ab-cursor-pointer" @click="useCopy(content)"><CopyDocument /></el-icon>
|
||||
</el-tooltip>
|
||||
{{ content }}
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/framework/hooks/useCopy'
|
||||
|
||||
const props = defineProps({
|
||||
content: { type: String, required: true,}, //内容
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!--文档预览组件-->
|
||||
<template>
|
||||
<el-dialog title="预览" :model-value="dialogVisible" :before-close="handleClose" :modal="false" :show-close="true" :close-on-click-modal="false" append-to-body>
|
||||
<template #title>
|
||||
<span class="dialog-title">
|
||||
<el-button type="primary" @click="downloadFile()">下载文件</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-if="['doc', 'docx'].includes(fileType)">
|
||||
<vue-office-docx :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
|
||||
</div>
|
||||
<div v-else-if="fileType == 'xlsx'">
|
||||
<vue-office-excel :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
|
||||
</div>
|
||||
<div v-else-if="fileType == 'pdf'">
|
||||
<vue-office-pdf :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="['png', 'jpg', 'jpeg','gif'].includes(fileType)">
|
||||
<el-image :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
|
||||
</div>
|
||||
|
||||
<div v-else="">
|
||||
<el-input v-model="textarea1" style="height: 70vh;overflow: auto" autosize type="textarea" :options="{width: '100%',overflow: 'auto'}" />
|
||||
</div>
|
||||
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
// 引入VueOffice组件
|
||||
import VueOfficeDocx from '@vue-office/docx'
|
||||
import VueOfficeExcel from '@vue-office/excel'
|
||||
import VueOfficePdf from '@vue-office/pdf'
|
||||
import XEUtils from 'xe-utils'
|
||||
|
||||
// 引入相关样式
|
||||
import '@vue-office/docx/lib/index.css'
|
||||
import '@vue-office/excel/lib/index.css'
|
||||
|
||||
const textarea1=ref('')
|
||||
const emit = defineEmits(["closePreviewDialog"]);
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
fileName: { type: String, default: '' },// 文件url
|
||||
fileType: { type: String, default: '' },// 文件url
|
||||
fileUrl: { type: String, default: '' },// 文件url
|
||||
dialogVisible: { type: Boolean, default: false },// 控制显示
|
||||
})
|
||||
|
||||
const downloadFile = () => {
|
||||
if(XEUtils.startsWith(props.fileUrl, './')||XEUtils.startsWith(props.fileUrl, '/')){
|
||||
window.location.href=props.fileUrl
|
||||
}else{
|
||||
window.location.href=props.fileUrl
|
||||
}
|
||||
}
|
||||
|
||||
const rendered = () => {
|
||||
console.log('渲染完成')
|
||||
}
|
||||
const errorHandler = (e:any) => {
|
||||
console.error('渲染失败',e)
|
||||
ElMessage({message: '文档渲染失败',type: 'error',})
|
||||
}
|
||||
const handleClose = (done) => {
|
||||
emit('closePreviewDialog', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!-- 动态表单组件 -->
|
||||
<!-- 新功能禁止使用该组件 -->
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
|
||||
<el-row :gutter="props.gutter">
|
||||
<template v-for="(value, key, index) in props.formModel">
|
||||
<el-col class="ab-p-b-5" v-if="'section'===fieldConfigMap.get(key)?.type" :span="24" v-show="isShow(fieldConfigMap.get(key))">
|
||||
<AbSection :title="fieldConfigMap.get(key).name"></AbSection>
|
||||
</el-col>
|
||||
<el-col v-else :span="fieldConfigMap.get(key)?.span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))">
|
||||
<template v-if="fieldConfigMap.get(key)?.slot">
|
||||
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
|
||||
<slot :name="fieldConfigMap.get(key)?.slot"></slot>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
|
||||
</template>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #title>
|
||||
</template>
|
||||
<el-col :span="24" class="ab-text-right ab-m-b-18">
|
||||
<slot v-if="$slots['button']" name="button"></slot>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="executeSave(formRef)">保存</el-button>
|
||||
<el-button type="default" @click="formReset()">重置</el-button>
|
||||
</template>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
|
||||
import AbSection from "@/framework/components/standard/AbSection/index.vue";
|
||||
const formRef = ref<FormInstance>() //表单引用
|
||||
const dFormRules = reactive<FormRules>({}) //验证规则
|
||||
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
|
||||
//组件传参
|
||||
let props = defineProps({
|
||||
formModel: { type: Object },//表单绑定数据
|
||||
fieldConfigs: { type: Array },//表单明细配置
|
||||
gutter: { type: Number, default: 0 },//栅格间隔
|
||||
span: { type: Number, default: 6 },//栅格占据的列数
|
||||
saveEvent: { type: Function },//保存事件
|
||||
resetEvent: { type: Function },//重置事件
|
||||
})
|
||||
|
||||
// 外部使用响应式函数,不需要监听
|
||||
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
|
||||
// console.log(newValue);
|
||||
// },{ deep: true }
|
||||
// )
|
||||
|
||||
/**是否显示 */
|
||||
const isShow = (fieldConfig) => {
|
||||
if(fieldConfig&&undefined!==fieldConfig.show&&!fieldConfig.show)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
/**数据处理 */
|
||||
const dataHandle = (fieldConfigs) => {
|
||||
for (let fc of fieldConfigs) {
|
||||
//转换map,方便获取数据
|
||||
fieldConfigMap.value.set(fc.key, fc)
|
||||
//设置表单验证规则
|
||||
if (undefined !== fc.rules) {
|
||||
dFormRules[fc.key] = fc.rules
|
||||
}
|
||||
//绑定表单默认数据
|
||||
if(!props.formModel[fc.key])
|
||||
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||
}
|
||||
}
|
||||
|
||||
dataHandle(props.fieldConfigs)
|
||||
|
||||
/**执行查询 */
|
||||
const executeSave = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.saveEvent) props.saveEvent()
|
||||
}
|
||||
|
||||
/**重置查询条件 */
|
||||
const formReset = () => {
|
||||
if (props.resetEvent) {
|
||||
props.resetEvent()
|
||||
return
|
||||
}
|
||||
for (let fc of props.fieldConfigs) {
|
||||
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- 动态表单明细组件 -->
|
||||
<!-- 新功能禁止使用该组件 -->
|
||||
<template>
|
||||
<el-form-item :label-width="props.fieldSet?.labelWidth" :label="props.fieldSet?.name" :prop="props.fieldSet?.key" :required="props.fieldSet?.required">
|
||||
<!-- 文本 -->
|
||||
<el-text v-if="props.fieldSet?.type==='text'" style="width: 100%;" v-model="formModel[props.fieldSet?.key]" >
|
||||
<Text :data="formModel[props.fieldSet?.key]"></Text>
|
||||
</el-text>
|
||||
<!-- 文本框 -->
|
||||
<el-input v-else-if="props.fieldSet?.type==='input'" v-model="formModel[props.fieldSet?.key]"
|
||||
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请输入'+props.fieldSet?.name"
|
||||
:disabled="props.fieldSet?.disabled" />
|
||||
<!-- 下拉框 -->
|
||||
<el-select v-else-if="props.fieldSet?.type==='select'" v-model="formModel[props.fieldSet?.key]"
|
||||
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请选择'+props.fieldSet?.name"
|
||||
:disabled="props.fieldSet?.disabled" clearable filterable style="width: 100%;">
|
||||
<el-option v-for="item in props.fieldSet?.listData" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
<!-- 单选 -->
|
||||
<el-radio-group v-else-if="props.fieldSet?.type==='radio'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
|
||||
<el-radio v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-radio>
|
||||
</el-radio-group>
|
||||
<!-- 多选 -->
|
||||
<el-checkbox-group v-else-if="props.fieldSet?.type==='checkbox'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
|
||||
<el-checkbox v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<!-- 时间选择器 -->
|
||||
<el-date-picker style="width: 100%;" v-else-if="dataPickerType.find((o: any) => o.type === props.fieldSet?.type) " v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled"
|
||||
:type="props.fieldSet?.type" unlink-panels range-separator="-"
|
||||
:start-placeholder="props.fieldSet?.placeholder?'开始'+props.fieldSet?.placeholder:'开始'+props.fieldSet?.name"
|
||||
:end-placeholder="props.fieldSet?.placeholder?'结束'+props.fieldSet?.placeholder:'结束'+props.fieldSet?.name"
|
||||
:value-format="dataPickerType.find((o: any) => o.type === props.fieldSet?.type).valueFormat" />
|
||||
<!-- 不指定类型、无类型不输出 -->
|
||||
<template v-else></template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Text from "@/framework/components/standard/Text/index.vue";
|
||||
/**时间选择器配置 */
|
||||
let dataPickerType=[
|
||||
{type:'year',valueFormat:'YYYY'},
|
||||
{type:'month',valueFormat:'YYYY-MM'},
|
||||
{type:'date' ,valueFormat:'YYYY-MM-DD'},
|
||||
{type:'dates' ,valueFormat:'YYYY-MM-DD'},
|
||||
{type:'datetime' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
|
||||
{type:'week' },
|
||||
{type:'datetimerange' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
|
||||
{type:'daterange' ,valueFormat:'YYYY-MM-DD'},
|
||||
{type:'monthrange',valueFormat:'YYYY-MM'},
|
||||
]
|
||||
let props = defineProps({
|
||||
formModel: {type:Object},//表单函数
|
||||
editable:{type:Boolean,default:true},//可编辑
|
||||
//当前字段设置
|
||||
fieldSet: {
|
||||
type:Object,
|
||||
default:{
|
||||
key:String,//字段Key
|
||||
name:String, //显示名称
|
||||
placeholder:String, //默认提示
|
||||
required:Boolean,//是否必填
|
||||
labelWidth:{type:String,default:'82px'},//标签宽度
|
||||
show: { type: Boolean, default: true },//是否显示
|
||||
disabled:Boolean,//是否禁用
|
||||
type:String,//输入类型,当前支持input、select、radio、checkbox
|
||||
//列表数据,当type为select、radio、checkbox时使用
|
||||
listData:{
|
||||
type:Array,
|
||||
default:[
|
||||
{
|
||||
value:String,//值
|
||||
text:String,//文本显示、
|
||||
disabled:Boolean,//是否禁用
|
||||
}
|
||||
]},
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- 动态查询表单组件 -->
|
||||
<!-- 新功能禁止使用该组件 -->
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<div class="more-filter" @click="criteriaShow = !criteriaShow" v-if="criteriaShowNumber < fieldConfigMap.size">
|
||||
{{ criteriaShow ? '收起' : '展开' }}更多筛选
|
||||
<el-icon v-if="criteriaShow"><ArrowDown /></el-icon>
|
||||
<el-icon v-else><ArrowUp /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
|
||||
<el-row :gutter="props.gutter">
|
||||
<template v-for="(value, key, index) in props.formModel">
|
||||
<el-col :span="fieldConfigMap.get(key).span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))&&(!(props.criteriaShowNumber-1<index)?true:criteriaShow)">
|
||||
<!-- 自定义插槽-->
|
||||
<template v-if="fieldConfigMap.get(key).slot">
|
||||
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
|
||||
<slot :name="fieldConfigMap.get(key).slot"></slot>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<!-- 固定类型-->
|
||||
<template v-else>
|
||||
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
|
||||
</template>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col :span="3" class="ab-text-right ab-m-b-18" :offset="criteriaShow ? props.queryCriteriaOffset[1] : props.queryCriteriaOffset[0]">
|
||||
<el-button type="primary" v-throttle:click.delay[1000]="()=>executeQuery(formRef)" >查询</el-button>
|
||||
<el-button type="default" @click="formReset()">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
|
||||
const criteriaShow = ref(false) //查询条件隐藏功能
|
||||
const formRef = ref<FormInstance>() //表单引用
|
||||
const dFormRules = reactive<FormRules>({}) //验证规则
|
||||
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
|
||||
//组件传参
|
||||
let props = defineProps({
|
||||
formModel: { type: Object },//表单绑定数据
|
||||
fieldConfigs: { type: Array },//表单明细配置
|
||||
gutter: { type: Number, default: 0 },//栅格间隔
|
||||
span: { type: Number, default: 6 },//栅格占据的列数
|
||||
queryEvent: { type: Function },//查询事件
|
||||
resetEvent: { type: Function },//重置事件
|
||||
queryCriteriaOffset: { type: Array, default: [0,6] },//查询条件偏移量,[收起偏移量,展开偏移量]
|
||||
criteriaShowNumber: { type: Number, default: 6 },//查询条件默认显示数量
|
||||
})
|
||||
|
||||
// 外部使用响应式函数,不需要监听
|
||||
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
|
||||
// console.log(newValue);
|
||||
// },{ deep: true }
|
||||
// )
|
||||
|
||||
/**是否显示 */
|
||||
const isShow = (fieldConfig) => {
|
||||
if(undefined!==fieldConfig.show&&!fieldConfig.show)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
/**数据处理 */
|
||||
const dataHandle = (fieldConfigs) => {
|
||||
for (let fc of fieldConfigs) {
|
||||
//转换map,方便获取数据
|
||||
fieldConfigMap.value.set(fc.key, fc)
|
||||
//设置表单验证规则
|
||||
if (undefined !== fc.rules) {
|
||||
dFormRules[fc.key] = fc.rules
|
||||
}
|
||||
//绑定表单默认数据
|
||||
if(!props.formModel[fc.key])
|
||||
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||
}
|
||||
}
|
||||
|
||||
dataHandle(props.fieldConfigs)
|
||||
|
||||
/**执行查询 */
|
||||
const executeQuery = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
|
||||
return
|
||||
}
|
||||
if (props.queryEvent) props.queryEvent()
|
||||
}
|
||||
|
||||
/**重置查询条件 */
|
||||
const formReset = () => {
|
||||
if (props.resetEvent) {
|
||||
props.resetEvent()
|
||||
return
|
||||
}
|
||||
for (let fc of props.fieldConfigs) {
|
||||
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!-- Excel上传组件 -->
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="文件上传" :destroy-on-close="true" width="580px" draggable top="1vh">
|
||||
<el-row justify="end">
|
||||
<el-col :span="24" style="text-align: center;">
|
||||
<el-text size="large">注意事项:上传文件必须符合模板格式要求,点此<el-link type="primary" @click="downloadFile"><el-text type="primary" size="large">下载模板</el-text></el-link >! </el-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-upload ref="fileUpload" :headers="ht" :data="props.data" drag :auto-upload="false" :show-file-list="true" :action="props.uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
|
||||
<el-row v-show="''!==uploadResult" style="margin-top:10px" justify="end">
|
||||
<el-col :span="24" style="text-align: right;">
|
||||
<el-alert type="success" :closable="false">
|
||||
<template #default>
|
||||
<el-text >
|
||||
上传结果:本次一共导入数据[<el-text type="primary">{{ uploadResult.totalNum }}</el-text>]条,
|
||||
成功[<el-text type="success" >{{ uploadResult.successNum }}</el-text>]条,
|
||||
<el-text v-if="uploadResult.updateNum>0" >修改[<el-text type="success" >{{ uploadResult.updateNum }}</el-text>]条,</el-text>
|
||||
失败[<el-text type="warning" >{{ uploadResult.failureNum }}</el-text>]条,
|
||||
<el-link type="primary" v-if="uploadResult.filePath!==''" :href="uploadResult.filePath" target="_blank">下载错误信息</el-link>
|
||||
</el-text>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row style="margin-top:10px" justify="end">
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="primary" @click="submitFile">上传</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {UploadRawFile } from 'element-plus'
|
||||
import { getAllHeader } from "@/framework/store/modules/user";
|
||||
|
||||
|
||||
const fileUpload=ref()
|
||||
|
||||
const ht=getAllHeader()
|
||||
|
||||
let props = defineProps({
|
||||
//上传时附带的额外参数
|
||||
data: {},
|
||||
//模板名称(模板需要放在public/templete)
|
||||
templateName: { type: String, default: '', },
|
||||
//文件上传地址
|
||||
uploadUrl: String,
|
||||
//是否自定义结果处理
|
||||
customResultHandle:{ type: Boolean, default: false },
|
||||
//上传文件最大值,单位(MB)
|
||||
uploadMaxSize: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
// 下载模板
|
||||
const downloadFile = () => {
|
||||
if (props.templateName === '') {
|
||||
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
|
||||
}
|
||||
let dom = document.createElement("a");
|
||||
dom.style.display = 'none'
|
||||
dom.target = '_blank'
|
||||
dom.href = `./templete/${props.templateName}`
|
||||
document.body.appendChild(dom);
|
||||
dom.click();
|
||||
document.body.removeChild(dom);
|
||||
}
|
||||
|
||||
const submitFile = () => {
|
||||
fileUpload.value.submit()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 文件上传之前判断
|
||||
* @param file 上传的文件
|
||||
* */
|
||||
const beforeExcelUpload = (file: UploadRawFile) => {
|
||||
uploadResult.value=''
|
||||
if(!['xls', 'xlsx'].includes(file.name.split('.').pop())){
|
||||
ElMessage.warning('上传文件只能是 xls / xlsx 格式' )
|
||||
return false;
|
||||
}
|
||||
if(props.uploadMaxSize>0&&file.size>(props.uploadMaxSize*1024*1024)){
|
||||
ElMessage.warning('上传文件大小不能超过'+props.uploadMaxSize+'MB!')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const uploadResult = ref('')
|
||||
|
||||
const emit = defineEmits(['updateCheck','resultHandleEvent'])
|
||||
const excelUploadSuccess = (response: any) => {
|
||||
const { message,status,data } = response;
|
||||
if(200===response.status){
|
||||
if(props.customResultHandle){
|
||||
dialogVisible.value=false
|
||||
emit('resultHandleEvent', data)
|
||||
}else{
|
||||
if(data.handleSuccess){
|
||||
uploadResult.value=data
|
||||
}else{
|
||||
ElMessage.error("Excel文件处理失败:"+data.message);
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
if (status === 401) {
|
||||
ElMessage.error('未通过身份验证');
|
||||
}
|
||||
const temp = String(status)[0]
|
||||
if (temp === '9') {
|
||||
ElMessage.error(message || '系统出错');
|
||||
console.error('系统异常', response.data);
|
||||
return
|
||||
}else{
|
||||
ElMessage.error('系统异常');
|
||||
console.error('系统异常', response.data);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传错误提示
|
||||
const excelUploadError = (response: any) => {
|
||||
console.error(response);
|
||||
if (response.status === 401) {
|
||||
ElMessage.error('未通过身份验证');
|
||||
}else{
|
||||
ElMessage.error('文件上传服务异常');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
dialogVisible
|
||||
});
|
||||
onMounted(() => {
|
||||
uploadResult.value=''
|
||||
});
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!-- 底部浮动-->
|
||||
<template >
|
||||
<div ref="bfRootRef" :key="isFloat? 'bottom-float':'bottom-fix'" :class="[isFloat ? 'bottom-float':'bottom-fix']" >
|
||||
<slot v-if="$slots['content']" name="content"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const bfRootRef= ref()
|
||||
//组件传参
|
||||
let props = defineProps({
|
||||
height: { type: String,default:'50px' },//浮动层高度
|
||||
backgroundColor: { type: String,default:'#fff' },//浮动层背景色
|
||||
getWidthEvent: { type: Function },//获取浮动层宽度
|
||||
})
|
||||
|
||||
const isFloat=ref(false)
|
||||
const width=ref('')
|
||||
const initScrollHeight=ref(0)
|
||||
const initClientHeight=ref(0)
|
||||
const handleScroll = () => {
|
||||
let clientHeight=document.documentElement.clientHeight || window.innerHeight
|
||||
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
|
||||
let temp=(scrollTop + clientHeight+bfRootRef.value.offsetHeight)
|
||||
isFloat.value=(temp >= initScrollHeight.value)?false:true
|
||||
}
|
||||
const windowChangesListener = () => {
|
||||
width.value=props.getWidthEvent()+'px'
|
||||
}
|
||||
onMounted(() => {
|
||||
width.value=props.getWidthEvent()+'px'
|
||||
initScrollHeight.value=document.documentElement.scrollHeight || document.body.scrollHeight
|
||||
initClientHeight.value=document.documentElement.clientHeight || window.innerHeight
|
||||
handleScroll()
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
window.addEventListener('resize', windowChangesListener)
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bottom-float{
|
||||
position: fixed;
|
||||
height: v-bind('props.height');
|
||||
width: v-bind('width');
|
||||
bottom: 0vh;
|
||||
background-color: v-bind('props.backgroundColor');
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.bottom-fix{
|
||||
height: v-bind('props.height');
|
||||
width: v-bind('width');
|
||||
background-color: v-bind('props.backgroundColor');
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--日志显示组件-->
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="12"><el-text tag="b" size="large">变更前内容</el-text></el-col>
|
||||
<el-col :span="12"><el-text tag="b" size="large">变更后内容</el-text></el-col>
|
||||
</el-row>
|
||||
<el-row style="height: 90%;margin-top: 10px;">
|
||||
<el-col :span="12">
|
||||
<div class="logContent">
|
||||
<!-- 优先插槽 -->
|
||||
<slot v-if="$slots['beforeContent']" name="beforeContent"></slot>
|
||||
<!-- 优先处理JSON格式 -->
|
||||
<pre v-else-if="isJsonString(props.beforeContent)">{{ JSON.parse(props.beforeContent)}}</pre>
|
||||
<!-- 默认输出 -->
|
||||
<el-text v-else="props.beforeContent">{{ props.beforeContent }}</el-text>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="logContent">
|
||||
<!-- 优先插槽 -->
|
||||
<slot v-if="$slots['content']" name="content"></slot>
|
||||
<!-- 优先处理JSON格式 -->
|
||||
<pre v-else-if="isJsonString(props.content)">{{ JSON.parse(props.content)}}</pre>
|
||||
<!-- 默认输出 -->
|
||||
<el-text v-else="props.content">{{ props.content }}</el-text>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isJsonString } from '@/framework/utils/standard/dataHandleUtil.ts'
|
||||
|
||||
let props = defineProps({
|
||||
//变更前内容
|
||||
beforeContent: { type: String, default: '', },
|
||||
//变更后内容
|
||||
content: { type: String, default: '', },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logContent{
|
||||
border:1px solid;
|
||||
border-color: #707175;
|
||||
border-radius:5px;
|
||||
height: 100%;
|
||||
width: 90%;
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
<!-- 实时搜索下拉框组件 -->
|
||||
<template>
|
||||
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable :remote-method="remoteMethod" :loading="loading" clearable
|
||||
:multiple="multiple" :collapse-tags="multiple" :collapse-tags-tooltip="multiple">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from 'vue'
|
||||
import { handleSelectData } from '@/framework/utils/standard/dataHandleUtil.ts'
|
||||
/**
|
||||
* 实时检索组件
|
||||
* SearchSelect
|
||||
* @author nielang
|
||||
* @description 用于实时检索的下拉框
|
||||
* @property {String} modelValue 使用v-model
|
||||
* @property {String} placeholder input的placeholder
|
||||
* @property {Boolean} multiple 是否支持多选
|
||||
* @property {Function} request 接口请求函数,默认请求参数{'keyword':'关键字'}
|
||||
* @property {Function} customRequestHandle 自定义请求参数处理
|
||||
* @property {Function} customResponseHandle 自定义响应结果处理
|
||||
* @property {Boolean} showOptionValue 选项中是否显示value属性,使用customResponseHandle后,该参数失效
|
||||
*/
|
||||
const props = defineProps({
|
||||
modelValue:{
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入关键字",
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
request:{
|
||||
type: Function,
|
||||
require: true
|
||||
},
|
||||
customRequestHandle: {
|
||||
type: Function,
|
||||
require: false
|
||||
},
|
||||
customResponseHandle: {
|
||||
type: Function,
|
||||
require: false
|
||||
},
|
||||
showOptionValue: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(false);
|
||||
interface ListItem {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const options = ref<ListItem[]>([])
|
||||
const remoteMethod = (keyword: string) => {
|
||||
if (keyword) {
|
||||
loading.value = true
|
||||
if (props.request) {
|
||||
let params:any = {keyword:keyword}
|
||||
//自定义请求参数处理
|
||||
if(props.customRequestHandle)
|
||||
params=props.customRequestHandle(params)
|
||||
props.request(params).then((data: any) => {
|
||||
//自定义响应结果处理
|
||||
if(props.customRequestHandle){
|
||||
options.value=props.customResponseHandle(data)
|
||||
}else{
|
||||
options.value=handleSelectData(data,props.showOptionValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
} else {
|
||||
options.value = []
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!--脱敏文本展示组件-->
|
||||
<template>
|
||||
<span :class="props.underline?'underline':''">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<template v-else-if="props.data" >
|
||||
<el-tooltip :content="props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data" placement="bottom" effect="light">
|
||||
{{ desensitize(props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data,props.type,props.custEvent) }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else> </template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { decrypt,desensitize,DesensitizationType } from '@/framework/utils/standard/security.ts'
|
||||
|
||||
let props = defineProps({
|
||||
data: { type: String, default: '', },//展示数据
|
||||
aesDecrypt:{type:Boolean,default: true},//是否aes解密
|
||||
decryptKey:{type:String,default: ''},//自定义密钥
|
||||
underline:{type:Boolean,default: false},//是否需要下划线
|
||||
type:DesensitizationType,//脱敏类型
|
||||
custEvent:{type: Function},//自定义脱敏方法
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.underline {
|
||||
border-bottom: 1px solid #CDD0D6;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!--文本展示组件-->
|
||||
<template>
|
||||
<span style="border-bottom: 1px solid #CDD0D6;width: 100%;display: block;">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<template v-else-if="props.data" >{{ props.data }}</template>
|
||||
<template v-else> </template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
let props = defineProps({
|
||||
data: { type: String, default: '', },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 防抖:在连续的操作中,无论进行了多长时间,只有某一次的操作后在指定的时间内没有再操作,这一次才被判定有效。
|
||||
* 使用:
|
||||
* <el-button v-debounce:click.delay[2000]="testDebounce">按钮防抖</el-button>
|
||||
* <el-button v-debounce:click.delay[2000]="()=>testDebounce('debounce')">按钮防抖传参</el-button>
|
||||
*/
|
||||
import { ObjectDirective } from 'vue'
|
||||
interface DebounceEl extends HTMLElement {
|
||||
debounceEvent: () => void
|
||||
}
|
||||
// 防抖指令封装
|
||||
const debounceDirective: ObjectDirective = {
|
||||
beforeMount(el: DebounceEl, binding: any) {
|
||||
let timer: number
|
||||
let delay: number = 1000
|
||||
const keys = Object.keys(binding.modifiers)
|
||||
if (keys) {
|
||||
const key: string | undefined = keys.find((key) => key.includes('delay'))?.toString()
|
||||
const value = (key ?? '').match(/(?<=\[).*?(?=\])/)
|
||||
if (value) delay = parseInt(value[0])
|
||||
}
|
||||
el.debounceEvent = () => {
|
||||
clearTimeout(timer)
|
||||
timer = window.setTimeout(() => {
|
||||
binding.value()
|
||||
}, delay)
|
||||
}
|
||||
el.addEventListener(binding.arg, el.debounceEvent)
|
||||
},
|
||||
beforeUnmount(el: DebounceEl, binding: any) {
|
||||
el.removeEventListener(binding.arg, el.debounceEvent)
|
||||
},
|
||||
}
|
||||
|
||||
export default debounceDirective
|
||||
@@ -0,0 +1,18 @@
|
||||
import { App, Directive } from "vue";
|
||||
import debounce from "./debounce";
|
||||
import throttle from "./throttle";
|
||||
|
||||
const directivesList: { [key: string]: Directive } = {
|
||||
debounce,
|
||||
throttle,
|
||||
};
|
||||
|
||||
const directives = {
|
||||
install: function (app: App<Element>) {
|
||||
Object.keys(directivesList).forEach(key => {
|
||||
app.directive(key, directivesList[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default directives;
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
/**
|
||||
*节流:规定时间内,只触发一次。比如我们设定500ms,在这个时间内,无论点击按钮多少次,它都只会触发一次。
|
||||
* 使用:
|
||||
* <el-button v-throttle:click.delay[2000]="testThrottle">按钮节流</el-button>
|
||||
* el-button v-throttle:click.delay[2000]="()=>testThrottle('throttle')">按钮节流传参</el-button>
|
||||
*
|
||||
*/
|
||||
import { ObjectDirective } from 'vue'
|
||||
|
||||
interface ThrottleEl extends HTMLElement {
|
||||
throttleEvent: () => void
|
||||
}
|
||||
|
||||
// 节流指令封装
|
||||
const throttleDirective: ObjectDirective = {
|
||||
beforeMount(el: ThrottleEl, binding: any) {
|
||||
let throttled = false
|
||||
let delay: number = 1000
|
||||
const keys = Object.keys(binding.modifiers)
|
||||
if (keys) {
|
||||
const key: string | undefined = keys.find((key) => key.includes('delay'))?.toString()
|
||||
const value = (key ?? '').match(/(?<=\[).*?(?=\])/)
|
||||
if (value) delay = parseInt(value[0])
|
||||
}
|
||||
el.throttleEvent = () => {
|
||||
if (!throttled) {
|
||||
binding.value()
|
||||
throttled = true
|
||||
setTimeout(() => {
|
||||
throttled = false
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
el.addEventListener(binding.arg, el.throttleEvent)
|
||||
},
|
||||
beforeUnmount(el: ThrottleEl, binding: any) {
|
||||
el.removeEventListener(binding.arg, el.throttleEvent)
|
||||
},
|
||||
}
|
||||
|
||||
export default throttleDirective
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
|
||||
export function useCopy(data: string){
|
||||
copy(data)
|
||||
ElMessage({type: 'success',message: '复制成功' })
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 跨页全选
|
||||
* @param multipleTableRef 多选的表格
|
||||
* @param showData 显示的数据
|
||||
* @param idKey 选中的id名称
|
||||
* @param tableFilterData 过滤掉不能选择数据后的全部数据,如果没有不能选中的数据,则为全部数据
|
||||
* @returns checkAll: 跨页全选状态
|
||||
* checkedIds 选中的所有行的id
|
||||
* initTableRowCheckStatus 初始化选中行
|
||||
* handlMultipageSelect 跨页全选状态操作
|
||||
* handleRowSelect 操作行选中
|
||||
* handleAllSelect 操作当前页选中
|
||||
*/
|
||||
export function useCrossPageSelect(multipleTableRef: any, showData: any, idKey: string, tableFilterData: any) {
|
||||
|
||||
const checkAll = ref(false) // 跨页全选转态
|
||||
const checkedIds = ref([]) // 选中的ids
|
||||
|
||||
// 初始化行选中状态
|
||||
const initTableRowCheckStatus = () => {
|
||||
nextTick(() => {
|
||||
showData.value.forEach((item: any) => {
|
||||
if (checkedIds.value.includes(item[idKey])) {
|
||||
multipleTableRef.value!.toggleRowSelection(item, true)
|
||||
return
|
||||
}
|
||||
multipleTableRef.value!.toggleRowSelection(item, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => showData.value, () => {
|
||||
initTableRowCheckStatus()
|
||||
})
|
||||
watch([checkedIds, tableFilterData], ([ids, tableData]) => {
|
||||
ids.length === tableData.length ? (checkAll.value = true) : (checkAll.value = false)
|
||||
}, { deep: true })
|
||||
|
||||
// 跨页全选
|
||||
const handlMultipageSelect = (status: boolean) => {
|
||||
if (status) {
|
||||
checkedIds.value = tableFilterData.value.map((item: any) => item[idKey])
|
||||
// checkedIds.value = tableData.value.filter(val => val.channel == "渠道销售").map(item => item.icapplynum)
|
||||
} else {
|
||||
checkedIds.value = []
|
||||
}
|
||||
initTableRowCheckStatus()
|
||||
}
|
||||
|
||||
// 切换选中行
|
||||
const handleRowSelect = (selection: any, row: any) => {
|
||||
const id = row[idKey]
|
||||
const item = selection.find((item: any) => item[idKey] === id)
|
||||
|
||||
if (item) {
|
||||
if (!checkedIds.value.includes(id)) {
|
||||
checkedIds.value.push(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
const index = checkedIds.value.indexOf(id)
|
||||
if (index !== -1) {
|
||||
checkedIds.value.splice(index, 1)
|
||||
}
|
||||
checkAll.value = false
|
||||
}
|
||||
// 单页全选
|
||||
const handleAllSelect = (selection: any) => {
|
||||
if (selection.length > 0) {
|
||||
selection.forEach((item: any) => {
|
||||
if (!checkedIds.value.includes(item[idKey])) {
|
||||
checkedIds.value.push(item[idKey])
|
||||
}
|
||||
});
|
||||
return
|
||||
}
|
||||
showData.value.forEach((item: any) => {
|
||||
const index = checkedIds.value.indexOf(item[idKey])
|
||||
if (index != -1) {
|
||||
checkedIds.value.splice(index, 1)
|
||||
}
|
||||
});
|
||||
checkAll.value = false
|
||||
}
|
||||
|
||||
return { checkAll, checkedIds, initTableRowCheckStatus, handlMultipageSelect, handleRowSelect, handleAllSelect }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import XEUtils from 'xe-utils'
|
||||
|
||||
/**
|
||||
* 表内检索,用于假分页时进行表内搜索
|
||||
* @param tableData 全部数据
|
||||
* @param keys 需要检索的key
|
||||
* @returns 检索后的数据,检索关键词,总数,分页对象
|
||||
*/
|
||||
export function useInTableSearchAndPaging(tableData: any, keys: Array<string> = []){
|
||||
|
||||
const tableKeyword = ref('')
|
||||
const debounceTableKeyword = refDebounced(tableKeyword, 500)
|
||||
const total = ref(0);
|
||||
const paging = ref({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
|
||||
const showData = computed(()=>{
|
||||
const filterData = tableData.value.filter((item: any) => {
|
||||
if (keys.length > 0) {
|
||||
const values = []
|
||||
for (const key in item) {
|
||||
if (keys.includes(key)) {
|
||||
values.push(item[key])
|
||||
}
|
||||
}
|
||||
return values.some(val => String(val).includes(debounceTableKeyword.value))
|
||||
}
|
||||
return Object.values(item).some(val => String(val).includes(debounceTableKeyword.value))
|
||||
})
|
||||
|
||||
const data = filterData.slice((paging.value.page - 1) * paging.value.pageSize, (paging.value.page - 1) * paging.value.pageSize + paging.value.pageSize)
|
||||
total.value = filterData.length
|
||||
return data
|
||||
})
|
||||
|
||||
return {tableKeyword, showData, total, paging}
|
||||
}
|
||||
|
||||
/**
|
||||
* VxeGrid表内检索,用于真分页时进行表内搜索
|
||||
* @param filterName 关键字对象
|
||||
* @param grid 表格对象
|
||||
* @param tableData 全部数据对象
|
||||
* @param searchProps 需要检索的key
|
||||
* @param options 选项,{children: 'children'}是否作为子节点展示
|
||||
* @param allTreeExpand 是否展开树形结构子节点
|
||||
*/
|
||||
export function useInVxeTableSearch(filterName:any, grid:any, tableData: any, searchProps: Array<string> = [],options:any,allTreeExpand:boolean=false){
|
||||
const filterVal = XEUtils.toValueString(filterName.value).trim().toLowerCase()
|
||||
const $table = grid.value
|
||||
if(!$table)
|
||||
return false
|
||||
if (filterVal) {
|
||||
if(searchProps.length==0){
|
||||
searchProps=Object.keys(tableData.value[0])
|
||||
}
|
||||
const rest = XEUtils.searchTree(tableData.value, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||
grid.value.loadData(rest)
|
||||
if(allTreeExpand){
|
||||
// 搜索之后默认展开所有子节点
|
||||
nextTick(() => {
|
||||
$table.setAllTreeExpand(true)
|
||||
})
|
||||
}
|
||||
}else {
|
||||
$table.loadData(tableData.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VxeGrid表内检索,用于真分页时进行表内搜索
|
||||
* @param filterName 关键字对象
|
||||
* @param grid 表格对象
|
||||
* @param tableData 全部数据对象
|
||||
* @param searchProps 需要检索的key
|
||||
* @param options 选项,{children: 'children'}是否作为子节点展示
|
||||
* @param allTreeExpand 是否展开树形结构子节点
|
||||
*/
|
||||
export function useInVxeTableSearchAndPageing(filterName:any, grid:any, tableData: any, searchProps: Array<string> = [],options:any,allTreeExpand:boolean=false,
|
||||
page:number,size:number){
|
||||
const filterVal = XEUtils.toValueString(filterName.value).trim().toLowerCase()
|
||||
const $table = grid.value
|
||||
if(!$table)
|
||||
return false
|
||||
if (filterVal) {
|
||||
if(searchProps.length==0){
|
||||
searchProps=Object.keys(tableData.value[0])
|
||||
}
|
||||
const rest = XEUtils.searchTree(tableData.value, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||
grid.value.loadData(rest.slice((page - 1) * size, page * size))//重新加载表格数据
|
||||
if(allTreeExpand){
|
||||
// 搜索之后默认展开所有子节点
|
||||
nextTick(() => {
|
||||
$table.setAllTreeExpand(true)
|
||||
})
|
||||
}
|
||||
}else {
|
||||
grid.value.loadData(tableData.value.slice((page - 1) * size, page * size))
|
||||
}
|
||||
}
|
||||
|
||||
export function useTableDataPageing(tableData: any,page:number,size:number){
|
||||
return tableData.slice((page - 1) * size, page * size)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { h } from 'vue'
|
||||
|
||||
export function useRenderHeader(node: any) {
|
||||
const { column } = node
|
||||
let realWidth = 0;
|
||||
let span = document.createElement('span');
|
||||
span.innerText = column.label;
|
||||
document.body.appendChild(span);
|
||||
realWidth = span.getBoundingClientRect().width;
|
||||
column.minWidth = realWidth+16; // 可能还有边距/边框等值,需要根据实际情况加上
|
||||
document.body.removeChild(span);
|
||||
|
||||
return h('span', column.label)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import XEUtils from 'xe-utils'
|
||||
|
||||
|
||||
/**表格相关数据查询重置 */
|
||||
export async function useTableQueryReload(tableRef:any){
|
||||
const params = tableRef.params
|
||||
if(params&¶ms.tableDataCache) params.tableDataCache=[] //重置表格数据缓存
|
||||
if(params&¶ms.filterNameRef) params.filterNameRef='' //重置表内搜索内容
|
||||
if(!XEUtils.isUndefined(tableRef.proxyConfig)){
|
||||
tableRef.commitProxy('reload') //重置表格相关数据
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* vxe代理请求参数处理
|
||||
* @param tableData 表所有数据
|
||||
* @param page 第几页
|
||||
* @param size 每页多少条
|
||||
* @returns
|
||||
*/
|
||||
export function useQueryParamsHandle(queryFrom:{},page: any, sorts:any, filters:any){
|
||||
const queryParams: any = Object.assign({}, queryFrom)
|
||||
// 处理分页条件
|
||||
if (page) {
|
||||
queryParams.page = page.currentPage
|
||||
queryParams.size = page.pageSize
|
||||
}
|
||||
// 处理排序条件
|
||||
const firstSort = sorts[0]
|
||||
if (firstSort) {
|
||||
queryParams.sort = firstSort.field
|
||||
queryParams.order = firstSort.order
|
||||
}
|
||||
// 处理筛选条件
|
||||
filters.forEach(({ field, values }) => {
|
||||
queryParams[field] = values.join(',')
|
||||
})
|
||||
return queryParams
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据过滤
|
||||
* @param $table 表格引用
|
||||
* @param tableData 需要过滤的数据
|
||||
* @param filterEvent 过滤成功事件
|
||||
* @param resetEvent 重置过滤数据事件
|
||||
* @returns
|
||||
*/
|
||||
function dataFilter($table:any,tableData:[],filterEvent:undefined,resetEvent:undefined){
|
||||
let tableParams=$table.params
|
||||
const filterVal = XEUtils.toValueString(tableParams.filterNameRef).trim().toLowerCase()
|
||||
if (filterVal) {
|
||||
let searchProps=tableParams.searchProps
|
||||
/**如需要检索的key不存在,则从表格对象中获取全部列字段名 */
|
||||
if(XEUtils.isUndefined(searchProps)||searchProps.length==0){
|
||||
searchProps=$table.getTableColumn().visibleColumn.map(function(vcolumn) {
|
||||
return vcolumn.field;
|
||||
});
|
||||
}
|
||||
let options={children: (XEUtils.isUndefined(tableParams.children)||!tableParams.children)?'no':''}
|
||||
const filterData = XEUtils.searchTree(tableData, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||
if(!XEUtils.isUndefined(filterEvent)&&'function'===typeof filterEvent){
|
||||
filterEvent(filterData)
|
||||
}
|
||||
return filterData
|
||||
}else {
|
||||
if(!XEUtils.isUndefined(resetEvent)&&'function'===typeof resetEvent){
|
||||
resetEvent(tableData)
|
||||
}
|
||||
return tableData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* VxeTable表内检索
|
||||
* @param tableRef 表格对象
|
||||
*/
|
||||
export function useInVxeTableSearch($table:any){
|
||||
let tableParams=$table.params
|
||||
const filterVal = XEUtils.toValueString(tableParams.filterNameRef).trim().toLowerCase()
|
||||
if (filterVal) {
|
||||
let searchProps=tableParams.searchProps
|
||||
/**如需要检索的key不存在,则从表格对象中获取全部列字段名 */
|
||||
if(XEUtils.isUndefined(searchProps)||searchProps.length==0){
|
||||
searchProps=$table.getTableColumn().visibleColumn.map(function(vcolumn) {
|
||||
return vcolumn.field;
|
||||
});
|
||||
}
|
||||
let options={children: (XEUtils.isUndefined(tableParams.children)||!tableParams.children)?'no':''}
|
||||
const filterData = XEUtils.searchTree(tableParams.tableDataCache, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||
return filterData
|
||||
}else {
|
||||
return tableParams.tableDataCache
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VxeGrid表内检索
|
||||
* 服务端分页(params.clientPage不配置或者配置为false),不会修改当前分页数据
|
||||
* @param tableRef 表格对象
|
||||
*/
|
||||
export function useInVxeGridSearch(tableRef:any){
|
||||
const $table = tableRef
|
||||
|
||||
if(!$table) {
|
||||
console.error('表格对象tableRef不存在');
|
||||
return false
|
||||
}
|
||||
if(!$table.params) {
|
||||
console.error('自定义参数配置params不存在');
|
||||
return false
|
||||
}
|
||||
let tableParams=$table.params
|
||||
/**客户端分页并且使用了代理配置,直接重新加载表格,通过useQueryAndClientPaging方法执行筛选操作 */
|
||||
if(tableParams.clientPage&&!XEUtils.isUndefined($table.proxyConfig)){
|
||||
$table.commitProxy('reload')
|
||||
return;
|
||||
}
|
||||
let tableData=[] //默认获取表格data中信息
|
||||
if(!XEUtils.isUndefined($table.data)){
|
||||
$table.data.forEach(function(o) {
|
||||
tableData.push(o)
|
||||
});
|
||||
}
|
||||
if(!XEUtils.isUndefined($table.getProxyInfo())&&null!=$table.getProxyInfo())
|
||||
tableData=$table.getProxyInfo().data //从表格代理信息中获取当前全量数据
|
||||
/**如tableDataRef不存在,则从表格对象中初始化全量数据-----已从表格代理信息中获取当前全量数据,该逻辑暂不使用*/
|
||||
// let tableData=tableParams.tableDataCache //待处理数据
|
||||
// if(null==tableData||tableData.length==0){
|
||||
// let visibleData=$table.getTableData().visibleData
|
||||
// tableData=new Array()
|
||||
// visibleData.forEach(function(v,k) {
|
||||
// tableData.push(v)
|
||||
// v.children.forEach(function(cv,ck) {
|
||||
// tableData.push(cv)
|
||||
// });
|
||||
// });
|
||||
// tableParams.tableDataCache=tableData
|
||||
// }
|
||||
dataFilter($table,tableData,
|
||||
function(filterData){
|
||||
$table.loadData(filterData)
|
||||
if(tableParams.allTreeExpand){
|
||||
// 搜索之后默认展开所有子节点
|
||||
nextTick(() => {
|
||||
$table.setAllTreeExpand(true)
|
||||
})
|
||||
}
|
||||
},function(resetData){
|
||||
$table.loadData(resetData)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据,并由客户端分页
|
||||
* @param tableRef 表格对象
|
||||
* @param page 分页信息
|
||||
* @param sorts 排序信息
|
||||
* @param requestMethod 执行查询的方法
|
||||
* @returns
|
||||
*/
|
||||
export async function useQueryAndClientPaging(tableRef:any,page: any, sorts:any, requestMethod:any){
|
||||
const $table = tableRef.value
|
||||
if(!$table) {
|
||||
console.error('表格对象tableRef不存在');
|
||||
return false
|
||||
}
|
||||
if(!$table.params) {
|
||||
console.error('自定义参数配置params不存在');
|
||||
return false
|
||||
}
|
||||
if(!$table.params.clientPage){
|
||||
console.error('自定义参数配置params.clientPage应该设置为true');
|
||||
return false
|
||||
}
|
||||
let cdata=$table.params.tableDataCache
|
||||
/**缓存中不存在数据,则发起请求,并初始化缓存 */
|
||||
if(cdata.length==0){
|
||||
let dataPromise= await requestMethod()
|
||||
$table.params.tableDataCache=dataPromise
|
||||
cdata=dataPromise
|
||||
}
|
||||
/**排序*/
|
||||
cdata=XEUtils.orderBy(cdata, sorts.map((sort:any) => [sort.field, sort.order]))
|
||||
/**如表内搜索存在关键字,则首先过滤数据 */
|
||||
cdata=dataFilter($table,cdata)
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
total:cdata.length,
|
||||
data:cdata.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/framework/store/modules/tagsView";
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-container" >
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath"/>
|
||||
</keep-alive>
|
||||
|
||||
</router-view>
|
||||
|
||||
<el-backtop target=".app-container" :right="30" :bottom="5" :visibility-height="50" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
// height: 100%;
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.fixed-tags+.app-container {
|
||||
// min-height: 100vh;
|
||||
padding-top: 54px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<!-- 菜单列表-->
|
||||
<el-menu :default-active="activeMenu" :router="true" mode="horizontal"
|
||||
:background-color="settingsStore.currentLayoutColor.navrbarBackgroundColor"
|
||||
:text-color="settingsStore.currentLayoutColor.menuFontTextColor"
|
||||
:active-text-color="settingsStore.currentLayoutColor.activeMenuFontTextColor" >
|
||||
<el-sub-menu v-for="item in routers" :key="item.path" :index="item.path" >
|
||||
<template #title>{{ item.meta.title }}</template>
|
||||
<el-menu-item v-for="twoitem in item.children" :key="twoitem.path" :index="item.path+'/'+twoitem.path">{{ twoitem.meta.title }}</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
const settingsStore = useSettingsStore();
|
||||
const routesStore = useRoutesStore();
|
||||
|
||||
|
||||
const routers = computed(() => {
|
||||
return routesStore.routes.filter((item: any) => {
|
||||
return item.meta && item.meta.title && item.children && item.children.length > 0
|
||||
})
|
||||
})
|
||||
|
||||
let activeMenu = ref('')
|
||||
let router = useRouter()
|
||||
watch(() => router,
|
||||
(newValue) => {
|
||||
activeMenu.value = newValue.currentRoute.value.fullPath
|
||||
},
|
||||
{ immediate: true,deep: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu {
|
||||
height: 50px;
|
||||
border-bottom: none;
|
||||
|
||||
&>.el-menu-item {
|
||||
border-bottom: none;
|
||||
|
||||
&.is-active {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="header-left">
|
||||
<!-- 系统LOGO -->
|
||||
<img :src="logoUrl" alt="logo" class="ab-m-r-50" v-if="settingsStore.showLogo"/>
|
||||
<!-- 子系统选择功能-->
|
||||
<SubSystemSwitch></SubSystemSwitch>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import logoPng from '@/assets/img/logo.png'
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import SubSystemSwitch from "@/framework/layoutpublic/SubSystemSwitch/index.vue"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
|
||||
|
||||
let logoUrl = logoPng
|
||||
if(''!=settingsStore.logoAddress){
|
||||
logoUrl=settingsStore.logoAddress
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.header-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
.system-menu{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.current-menu{
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.other-menu{
|
||||
width: 480px;
|
||||
height: auto;
|
||||
color: rgba(255,255,255,0.45);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 45px;
|
||||
.item{
|
||||
width: 115px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 10px;
|
||||
&:hover{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.active{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="header-right">
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<HeaderSearch />
|
||||
<!-- 信息通知组件 -->
|
||||
<MessageNotify />
|
||||
<!-- 登录人信息 -->
|
||||
<NewUserDropDown></NewUserDropDown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderSearch from './Search.vue'
|
||||
import MessageNotify from '../MessageNotify/index.vue'
|
||||
import headPng from '@/assets/img/head.jpg'
|
||||
import { useUserStore } from "@/framework/store/modules/user";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import NewUserDropDown from "@/framework/layoutpublic/NewUserDropDown/index.vue"
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
let headAddress = headPng
|
||||
|
||||
if(''!=settingsStore.headAddress){
|
||||
headAddress=settingsStore.headAddress
|
||||
}
|
||||
|
||||
const logout = () =>{
|
||||
userStore.logout()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-right {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
.name {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div :class="{ 'show': show }" class="header-search">
|
||||
<el-tooltip content="菜单搜索" placement="bottom" effect="light">
|
||||
<el-icon @click.stop="click">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-select ref="headerSearchSelect" v-model="search" :remote-method="querySearch" filterable default-first-option
|
||||
remote placeholder="请输入菜单名称" class="header-search-select" @change="change">
|
||||
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.item.title.join(' > ')" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Fuse from 'fuse.js'
|
||||
import path from 'path-browserify';
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
const routesStore = useRoutesStore();
|
||||
let router = useRouter()
|
||||
|
||||
const search = ref('')
|
||||
const options = ref([])
|
||||
const searchPool = ref([])
|
||||
const show = ref(false)
|
||||
const fuse = ref({})
|
||||
|
||||
const routes = computed(() => {
|
||||
return routesStore.routes.filter((item: any) => {
|
||||
return item.meta && !item.meta.hidden && item.meta.title && item.children && item.children.length > 0
|
||||
})
|
||||
})
|
||||
|
||||
watch(searchPool, (list) => {
|
||||
initFuse(list)
|
||||
})
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', close)
|
||||
} else {
|
||||
document.body.removeEventListener('click', close)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const generateRoutes = (routes: any, basePath = '/', prefixTitle = []) => {
|
||||
let res: any = []
|
||||
|
||||
for (const router of routes) {
|
||||
const data:any = {
|
||||
path: path.resolve(basePath, router.path),
|
||||
title: [...prefixTitle]
|
||||
}
|
||||
|
||||
if (router.meta && router.meta.title) {
|
||||
data.title = [...data.title, router.meta.title]
|
||||
res.push(data)
|
||||
}
|
||||
if (router.children) {
|
||||
const tempRoutes: any = generateRoutes(router.children, data.path, data.title)
|
||||
if (tempRoutes.length >= 1) {
|
||||
res = [...res, ...tempRoutes]
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
searchPool.value = generateRoutes(routes.value)
|
||||
|
||||
const initFuse = (list: any) => {
|
||||
fuse.value = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
// maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [{
|
||||
name: 'title',
|
||||
weight: 0.7
|
||||
}, {
|
||||
name: 'path',
|
||||
weight: 0.3
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const headerSearchSelect:any = ref(null)
|
||||
|
||||
const click = () => {
|
||||
show.value = !show.value
|
||||
if (show.value) {
|
||||
headerSearchSelect.value && headerSearchSelect.value.focus()
|
||||
}
|
||||
}
|
||||
const close = () => {
|
||||
headerSearchSelect.value && headerSearchSelect.value.blur()
|
||||
options.value = []
|
||||
show.value = false
|
||||
}
|
||||
const change = (val: any) => {
|
||||
router .push(val.item.path)
|
||||
search.value = ''
|
||||
options.value = []
|
||||
nextTick(() => {
|
||||
show.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const querySearch = (query) => {
|
||||
if (query !== '') {
|
||||
options.value = fuse.value.search(query)
|
||||
} else {
|
||||
options.value = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-search {
|
||||
font-size: 1.3rem;
|
||||
margin-right: 32px;
|
||||
margin-top: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-search-select {
|
||||
font-size: 18px;
|
||||
transition: width 0.2s;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
.el-input__inner {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
.header-search-select {
|
||||
width: 210px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<HeaderLeft />
|
||||
<div class="center">
|
||||
<HeaderCenter />
|
||||
</div>
|
||||
<HeaderRight />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderLeft from "./Left.vue"
|
||||
import HeaderRight from "./Right.vue"
|
||||
import HeaderCenter from "./Center.vue"
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.header-container{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.center{
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="message-notify" v-if="settingsStore.showMessageNotify">
|
||||
<el-badge :value="2" style="font-size: 1.3rem;">
|
||||
<el-icon class="icon" @mouseenter="showNotify = true" @mouseleave="onLeave">
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
|
||||
<div class="msg-list" v-show="showNotify" @mouseenter="onEnter" @mouseleave="showNotify = false">
|
||||
<div class="ab-m-b-10">
|
||||
<div class="msg-item" v-for="item in list" :key="item.id">
|
||||
<div class="left">
|
||||
<el-icon style="font-size: 1.3rem;">
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div>{{ item.title }}</div>
|
||||
<div class="date">{{ item.date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="left">清空通知</div>
|
||||
<div class="right">查看更多</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const showNotify = ref(false)
|
||||
let timer: number = 0
|
||||
const onEnter = () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
const onLeave = () => {
|
||||
timer = window.setTimeout(() => {
|
||||
showNotify.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const list = ref([{
|
||||
title: '你有十五条售后问题要处理',
|
||||
date: '2023-06-30 10:21'
|
||||
},{
|
||||
title: '你有十五条售后问题要处理',
|
||||
date: '2023-06-30 10:21'
|
||||
}])
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-notify {
|
||||
|
||||
cursor: pointer;
|
||||
margin-right: 32px;
|
||||
margin-top: 3px;
|
||||
|
||||
.msg-list {
|
||||
width: 350px;
|
||||
height: auto;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
color: #333;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 70px;
|
||||
right: 50px;
|
||||
padding-top: 10px;
|
||||
|
||||
.msg-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
&:hover{
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
.right{
|
||||
font-size: 14px;
|
||||
color: #303335;
|
||||
.date{
|
||||
margin-top: 3px;
|
||||
color: #b8bbbd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer{
|
||||
display: flex;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
.left{
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
&:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.right{
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
&:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<!-- 左侧二级菜单 -->
|
||||
<div class="sidebar-container" :class="{'sidebar-container-close': !appStore.sidebar.opened}">
|
||||
<div class="top">
|
||||
<span class="menu-name" v-show="appStore.sidebar.opened">{{ activeMenuName }}</span>
|
||||
<Hamburger :is-active="appStore.sidebar.opened" @toggle-click="toggleSideBar" />
|
||||
</div>
|
||||
|
||||
<el-menu :default-active="activePath" :router="true" background-color="#F6F8FB" text-color="#303335" :collapse="!appStore.sidebar.opened" :collapse-transition="false">
|
||||
<el-menu-item v-for="item in subRoutes" :key="resolvePath(item.path)" :index="resolvePath(item.path)" >
|
||||
<template v-if="!appStore.sidebar.opened">
|
||||
{{ item.meta.title }}
|
||||
</template>
|
||||
<template #title >{{ item.meta.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Hamburger from "@/framework/components/Hamburger/index.vue";
|
||||
import path from 'path-browserify';
|
||||
import { isExternal } from '@/framework/utils/index';
|
||||
import { ref, watch} from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
const routesStore = useRoutesStore();
|
||||
|
||||
import { useAppStore } from "@/framework/store/modules/app";
|
||||
const appStore = useAppStore()
|
||||
|
||||
/**
|
||||
* 左侧菜单栏显示/隐藏
|
||||
*/
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar(!appStore.sidebar.opened);
|
||||
}
|
||||
|
||||
let activePath= ref('')
|
||||
let activeMenuName = ref('')
|
||||
let activeMenuPath = ref('')
|
||||
let subRoutes:any = ref([])
|
||||
|
||||
|
||||
let router = useRouter()
|
||||
watch(
|
||||
() => router,
|
||||
(newValue) => {
|
||||
activePath.value = newValue.currentRoute.value.fullPath
|
||||
const path = newValue.currentRoute.value.matched[0].path
|
||||
const activeRoute = routesStore.routes.find((item: any) => item.path === path)
|
||||
if (activeRoute) {
|
||||
activeMenuName.value = activeRoute.meta.title
|
||||
activeMenuPath.value = activeRoute.path
|
||||
subRoutes.value = activeRoute.children
|
||||
}
|
||||
},
|
||||
{ immediate: true,deep: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 解析路径
|
||||
*
|
||||
* @param routePath 路由路径
|
||||
*/
|
||||
function resolvePath(routePath: string) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
||||
const fullPath = path.resolve(activeMenuPath.value, routePath); // 相对路径 → 绝对路径
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-container {
|
||||
width: $sideBarWidth;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x:hidden;
|
||||
background-color: #F6F8FB;
|
||||
|
||||
.top{
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.menu-name{
|
||||
padding-left: 16px;
|
||||
color: #303335;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.el-tooltip__trigger) {
|
||||
display: block;
|
||||
width: 64px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sidebar-container-close{
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.el-menu--vertical{
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<!-- 标签页 -->
|
||||
<div class="tags-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<router-link v-for="tag in visitedViews" :key="tag.path" :class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
:data-path="tag.path" :to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent="openTagMenu(tag, $event)">
|
||||
{{ tag.meta?.title }}
|
||||
<span v-if="!isAffix(tag)" class="tags-item-close" @click.prevent.stop="closeSelectedTag(tag)">×</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- tag标签操作菜单 -->
|
||||
<ul v-show="tagMenuVisible" class="tag-menu" :style="{ left: left + 'px', top: top + 'px' }">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
刷新
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
关闭
|
||||
</li>
|
||||
<li @click="closeOtherTags">
|
||||
关闭其它标签页
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
关闭左侧标签页
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
关闭右侧标签页
|
||||
</li>
|
||||
<li @click="closeAllTags()">
|
||||
关闭所有标签页
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
} from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import path from "path-browserify";
|
||||
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { usePermissionStore } from "@/framework/store/modules/permission";
|
||||
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
const selectedTag = ref({});
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
},
|
||||
{
|
||||
//初始化立即执行
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||
watch(tagMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
}
|
||||
});
|
||||
|
||||
function filterAffixTags(routes: any[], basePath = "/") {
|
||||
let tags: TagView[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path);
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTags() {
|
||||
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTags() {
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of tagsViewStore.visitedViews) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
}
|
||||
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[1].fullPath ||
|
||||
(selectedTag.value as TagView).fullPath === "/index"
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLastView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
tagsViewStore.delCachedView(view);
|
||||
const { fullPath } = view;
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[]) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
router.replace({ path: "/redirect/" })
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view: TagView) {
|
||||
tagsViewStore.delView(view).then((res: any) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOtherTags() {
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllTags() {
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
toLastView(res.visitedViews);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
import { useAppStore } from "@/framework/store/modules/app";
|
||||
const appStore = useAppStore()
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
left.value = e.clientX;
|
||||
top.value = e.clientY - 40;
|
||||
tagMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||
position: relative;
|
||||
|
||||
.tags-item {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
margin: 4px 0 0 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
content: "";
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
border-radius: 100%;
|
||||
// font-size: 16px;
|
||||
padding: 0 4px;
|
||||
transform: scale(2);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgb(0 0 0 / 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-menu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-container style="height: 100%;">
|
||||
<el-header :class="{ 'fixed-header': fixedHeader }"
|
||||
:style="`background-color:${settingsStore.currentLayoutColor.navrbarBackgroundColor}`" height="50px">
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-container class="content-wrapper" style="height: 100%;" :style="{ marginTop: fixedHeader ? '50px' : 0 }">
|
||||
<el-main class="ab-p-0 main-wrapper" >
|
||||
<div :class="{ 'fixed-tags': fixedTags && showTagsView }">
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
<app-main style="width: 100%;padding-left: 10px;padding-right: 10px;"/>
|
||||
<el-backtop target=".content-wrapper .main-wrapper" :right="30" :bottom="5" :visibility-height="50" />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Header from "./components/Header/index.vue";
|
||||
import AppMain from "./components/AppMain.vue";
|
||||
import TagsView from "./components/TagsView/index.vue";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const fixedTags = computed(() => settingsStore.fixedTags);
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.fixed-tags {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<section class="app-container" >
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<!-- iframe页 -->
|
||||
<ExtView v-show="currentRoute.meta.external" :currentRoute="currentRoute" :refreshPage="refreshPage"></ExtView>
|
||||
|
||||
<el-backtop target=".app-container" :right="30" :bottom="5" :visibility-height="50" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/framework/store/modules/tagsView";
|
||||
import ExtView from "@/framework/views/ext/morePage.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const routesStore = useRoutesStore();
|
||||
|
||||
const currentRoute=ref()
|
||||
const refreshPage=ref(false)
|
||||
let router = useRouter()
|
||||
watch(
|
||||
() => router,
|
||||
(newValue) => {
|
||||
|
||||
const fullPath = newValue.currentRoute.value.fullPath
|
||||
const menuPath = newValue.currentRoute.value.matched[0].path
|
||||
const activeRoute = routesStore.routes.find((item: any) => item.path === menuPath)
|
||||
|
||||
if(currentRoute.value&&(fullPath===(menuPath+ '/' + currentRoute.value.path))){
|
||||
refreshPage.value=true;
|
||||
}else{
|
||||
refreshPage.value=false;
|
||||
}
|
||||
|
||||
if (activeRoute) {
|
||||
activeRoute.children.forEach((o: any) => {
|
||||
if (fullPath == ( menuPath + '/' + o.path)) {
|
||||
currentRoute.value=o
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
// min-height: calc(100vh - 64px);
|
||||
background-color: #f4f4f4;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.fixed-tags+.app-container {
|
||||
// min-height: 100vh;
|
||||
padding-top: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<!-- 菜单列表-->
|
||||
<el-menu :default-active="activeMenu" :router="true" mode="horizontal"
|
||||
:background-color="settingsStore.currentLayoutColor.navrbarBackgroundColor"
|
||||
:text-color="settingsStore.currentLayoutColor.menuFontTextColor"
|
||||
:active-text-color="settingsStore.currentLayoutColor.activeMenuFontTextColor">
|
||||
<el-menu-item v-for="item in routers" :key="item.path" :index="item.path">
|
||||
<el-badge :show-zero="false" :max="99" :offset="[5, 15]">
|
||||
{{ item.meta.title }}
|
||||
</el-badge>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
const settingsStore = useSettingsStore();
|
||||
const routesStore = useRoutesStore();
|
||||
|
||||
const routers = computed(() => {
|
||||
return routesStore.routes.filter((item: any) => {
|
||||
return item.meta && item.meta.title && item.children && item.children.length > 0
|
||||
})
|
||||
})
|
||||
|
||||
const activeMenuBackgroundColor=ref(settingsStore.currentLayoutColor.activeMenuBackgroundColor)
|
||||
|
||||
let activeMenu = ref('')
|
||||
let router = useRouter()
|
||||
watch(() => router,
|
||||
(newValue) => {
|
||||
activeMenu.value = newValue.currentRoute.value.matched[0].path
|
||||
},
|
||||
{ immediate: true,deep: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu {
|
||||
height: 64px;
|
||||
border-bottom: none;
|
||||
|
||||
&>.el-menu-item {
|
||||
border-bottom: none;
|
||||
|
||||
&.is-active {
|
||||
border-bottom: none;
|
||||
background-color: v-bind(activeMenuBackgroundColor) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="header-left">
|
||||
<!-- 系统LOGO -->
|
||||
<img :src="logoUrl" style="width:155px;height:35px" alt="logo" class="ab-m-r-50" v-if="settingsStore.showLogo"/>
|
||||
<!-- 子系统选择功能-->
|
||||
<SubSystemSwitch></SubSystemSwitch>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import logoPng from '@/assets/img/logo.png'
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import SubSystemSwitch from "@/framework/layoutpublic/SubSystemSwitch/index.vue"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
let logoUrl = logoPng
|
||||
if(''!=settingsStore.logoAddress){
|
||||
logoUrl=settingsStore.logoAddress
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.header-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="header-right">
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<HeaderSearch />
|
||||
<!-- 信息通知组件 -->
|
||||
<MessageNotify />
|
||||
<!-- 登录人信息 -->
|
||||
<NewUserDropDown></NewUserDropDown>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderSearch from './Search.vue'
|
||||
import MessageNotify from '../MessageNotify/index.vue'
|
||||
import NewUserDropDown from "@/framework/layoutpublic/NewUserDropDown/index.vue"
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-right {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div :class="{ 'show': show }" class="header-search">
|
||||
<el-tooltip content="菜单搜索" placement="bottom" effect="light">
|
||||
<el-icon @click.stop="click">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-select ref="headerSearchSelect" v-model="search" :remote-method="querySearch" filterable default-first-option
|
||||
remote placeholder="Search" class="header-search-select" @change="change">
|
||||
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.item.title.join(' > ')" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Fuse from 'fuse.js'
|
||||
import path from 'path-browserify';
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
const routesStore = useRoutesStore();
|
||||
let router = useRouter()
|
||||
|
||||
const search = ref('')
|
||||
const options = ref([])
|
||||
const searchPool = ref([])
|
||||
const show = ref(false)
|
||||
const fuse = ref({})
|
||||
|
||||
const routes = computed(() => {
|
||||
return routesStore.routes.filter((item: any) => {
|
||||
return item.meta && !item.meta.hidden && item.meta.title && item.children && item.children.length > 0
|
||||
})
|
||||
})
|
||||
|
||||
watch(searchPool, (list) => {
|
||||
initFuse(list)
|
||||
})
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', close)
|
||||
} else {
|
||||
document.body.removeEventListener('click', close)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const generateRoutes = (routes: any, basePath = '/', prefixTitle = []) => {
|
||||
let res: any = []
|
||||
|
||||
for (const router of routes) {
|
||||
const data:any = {
|
||||
path: path.resolve(basePath, router.path),
|
||||
title: [...prefixTitle]
|
||||
}
|
||||
|
||||
if (router.meta && router.meta.title) {
|
||||
data.title = [...data.title, router.meta.title]
|
||||
res.push(data)
|
||||
}
|
||||
if (router.children) {
|
||||
const tempRoutes: any = generateRoutes(router.children, data.path, data.title)
|
||||
if (tempRoutes.length >= 1) {
|
||||
res = [...res, ...tempRoutes]
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
searchPool.value = generateRoutes(routes.value)
|
||||
|
||||
const initFuse = (list: any) => {
|
||||
fuse.value = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
// maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [{
|
||||
name: 'title',
|
||||
weight: 0.7
|
||||
}, {
|
||||
name: 'path',
|
||||
weight: 0.3
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const headerSearchSelect:any = ref(null)
|
||||
|
||||
const click = () => {
|
||||
show.value = !show.value
|
||||
if (show.value) {
|
||||
headerSearchSelect.value && headerSearchSelect.value.focus()
|
||||
}
|
||||
}
|
||||
const close = () => {
|
||||
headerSearchSelect.value && headerSearchSelect.value.blur()
|
||||
options.value = []
|
||||
show.value = false
|
||||
}
|
||||
const change = (val: any) => {
|
||||
router .push(val.item.path)
|
||||
search.value = ''
|
||||
options.value = []
|
||||
nextTick(() => {
|
||||
show.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const querySearch = (query) => {
|
||||
if (query !== '') {
|
||||
options.value = fuse.value.search(query)
|
||||
} else {
|
||||
options.value = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-search {
|
||||
font-size: 1.3rem;
|
||||
margin-right: 32px;
|
||||
margin-top: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-search-select {
|
||||
font-size: 18px;
|
||||
transition: width 0.2s;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
.el-input__inner {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
.header-search-select {
|
||||
width: 210px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<HeaderLeft />
|
||||
<div class="center">
|
||||
<HeaderCenter />
|
||||
</div>
|
||||
<HeaderRight />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderLeft from "./Left.vue"
|
||||
import HeaderRight from "./Right.vue"
|
||||
import HeaderCenter from "./Center.vue"
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.header-container{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.center{
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="message-notify" v-if="settingsStore.showMessageNotify">
|
||||
<el-badge :value="2" style="font-size: 1.3rem;">
|
||||
<el-icon class="icon" @mouseenter="showNotify = true" @mouseleave="onLeave">
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
|
||||
<div class="msg-list" v-show="showNotify" @mouseenter="onEnter" @mouseleave="showNotify = false">
|
||||
<div class="ab-m-b-10">
|
||||
<div class="msg-item" v-for="item in list" :key="item.id">
|
||||
<div class="left">
|
||||
<el-icon style="font-size: 1.3rem;">
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div>{{ item.title }}</div>
|
||||
<div class="date">{{ item.date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="left">清空通知</div>
|
||||
<div class="right">查看更多</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const showNotify = ref(false)
|
||||
let timer: number = 0
|
||||
const onEnter = () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
const onLeave = () => {
|
||||
timer = window.setTimeout(() => {
|
||||
showNotify.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const list = ref([{
|
||||
title: '你有十五条售后问题要处理',
|
||||
date: '2023-06-30 10:21'
|
||||
},{
|
||||
title: '你有十五条售后问题要处理',
|
||||
date: '2023-06-30 10:21'
|
||||
}])
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-notify {
|
||||
|
||||
cursor: pointer;
|
||||
margin-right: 32px;
|
||||
margin-top: 3px;
|
||||
|
||||
.msg-list {
|
||||
width: 350px;
|
||||
height: auto;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
color: #333;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 70px;
|
||||
right: 50px;
|
||||
padding-top: 10px;
|
||||
|
||||
.msg-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
&:hover{
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
.right{
|
||||
font-size: 14px;
|
||||
color: #303335;
|
||||
.date{
|
||||
margin-top: 3px;
|
||||
color: #b8bbbd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer{
|
||||
display: flex;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
.left{
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
&:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.right{
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
&:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<!-- 左侧二级菜单 v-show="subRoutes.length>1" -->
|
||||
<div class="sidebar-container" :class="{'sidebar-container-close': !appStore.sidebar.opened}">
|
||||
<div class="top">
|
||||
<span class="menu-name" v-show="appStore.sidebar.opened">{{ activeMenuName }}</span>
|
||||
<Hamburger :is-active="appStore.sidebar.opened" @toggle-click="toggleSideBar" />
|
||||
</div>
|
||||
|
||||
<el-menu :default-active="activePath" :router="true" background-color="#F6F8FB" text-color="#303335" :collapse="!appStore.sidebar.opened" :collapse-transition="false" :class="{'menu-list-wrap': !appStore.sidebar.opened}">
|
||||
<el-menu-item v-for="item in subRoutes" :key="resolvePath(item.path)" :index="resolvePath(item.path)" >
|
||||
<template v-if="!appStore.sidebar.opened">
|
||||
<el-badge :show-zero="false" :max="99" :offset="[5, 15]">
|
||||
{{ item.meta.title }}
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #title >
|
||||
<el-badge :show-zero="false" :max="99" :offset="[5, 15]">
|
||||
{{ item.meta.title }}
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Hamburger from "@/framework/components/Hamburger/index.vue";
|
||||
import path from 'path-browserify';
|
||||
import { isExternal } from '@/framework/utils/index';
|
||||
import { ref, watch} from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||
import { useAppStore } from "@/framework/store/modules/app";
|
||||
|
||||
const routesStore = useRoutesStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
/**
|
||||
* 左侧菜单栏显示/隐藏
|
||||
*/
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar(!appStore.sidebar.opened);
|
||||
}
|
||||
|
||||
let activePath= ref('')
|
||||
let activeMenuName = ref('')
|
||||
let activeMenuPath = ref('')
|
||||
let subRoutes:any = ref([])
|
||||
|
||||
let router = useRouter()
|
||||
watch(
|
||||
() => router,
|
||||
(newValue) => {
|
||||
activePath.value = newValue.currentRoute.value.fullPath
|
||||
const path = newValue.currentRoute.value.matched[0].path
|
||||
const activeRoute = routesStore.routes.find((item: any) => item.path === path)
|
||||
if (activeRoute) {
|
||||
activeMenuName.value = activeRoute.meta.title
|
||||
activeMenuPath.value = activeRoute.path
|
||||
subRoutes.value = activeRoute.children
|
||||
appStore.sidebar.moduleNumber=activeRoute.children.length
|
||||
// if(activeRoute.children.length<=1){
|
||||
// appStore.sidebar.opened=false
|
||||
// }
|
||||
}
|
||||
},
|
||||
{ immediate: true,deep: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 解析路径
|
||||
*
|
||||
* @param routePath 路由路径
|
||||
*/
|
||||
function resolvePath(routePath: string) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
||||
const fullPath = path.resolve(activeMenuPath.value, routePath); // 相对路径 → 绝对路径
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-container {
|
||||
width: $sideBarWidth;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x:hidden;
|
||||
background-color: #fff;
|
||||
// margin-right: 8px;
|
||||
padding: 0 8px;
|
||||
|
||||
.top{
|
||||
height: 64px;
|
||||
// border-bottom: 1px solid #F0F0F0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.menu-name{
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding-left: 16px;
|
||||
color: #303335;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.el-tooltip__trigger) {
|
||||
display: block;
|
||||
width: 64px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sidebar-container-close{
|
||||
width: 64px;
|
||||
}
|
||||
.el-menu{
|
||||
background-color: #fff;
|
||||
}
|
||||
.el-menu--vertical{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-menu-item{
|
||||
border-radius: 12x;
|
||||
}
|
||||
.el-menu-item:hover{
|
||||
background-color: #EDF0F5;
|
||||
// border-radius: 12px;
|
||||
// color: #2467E8;
|
||||
}
|
||||
.el-menu-item.is-active{
|
||||
background-color: #DEE8FC;
|
||||
// border-radius: 12px;
|
||||
color: #2467E8;
|
||||
position: relative;
|
||||
|
||||
&::after{
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 22px;
|
||||
background-color: #2467E8;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list-wrap .is-active::after{
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<router-link v-for="tag in visitedViews" :key="tag.path" :class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
:data-path="tag.path" :to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent="openTagMenu(tag, $event)">
|
||||
{{ tag.meta?.title }}
|
||||
|
||||
<el-icon v-if="!isAffix(tag)" class="tags-item-close" @click.prevent.stop="closeSelectedTag(tag)"><Close /></el-icon>
|
||||
<!-- <span v-if="!isAffix(tag)" class="tags-item-close" @click.prevent.stop="closeSelectedTag(tag)">×</span> -->
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- tag标签操作菜单 -->
|
||||
<ul v-show="tagMenuVisible" class="tag-menu" :style="{ left: left + 'px', top: top + 'px' }">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
刷新
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
关闭
|
||||
</li>
|
||||
<li @click="closeOtherTags">
|
||||
关闭其它标签页
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
关闭左侧标签页
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
关闭右侧标签页
|
||||
</li>
|
||||
<li @click="closeAllTags()">
|
||||
关闭所有标签页
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
ComponentInternalInstance,
|
||||
} from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import path from "path-browserify";
|
||||
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { usePermissionStore } from "@/framework/store/modules/permission";
|
||||
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||
import { removeSlashes } from "@/framework/utils/standard/dataHandleUtil";
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
const selectedTag = ref({});
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
},
|
||||
{
|
||||
//初始化立即执行
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||
watch(tagMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
}
|
||||
});
|
||||
|
||||
function filterAffixTags(routes: any[], basePath = "/") {
|
||||
let tags: TagView[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path);
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTags() {
|
||||
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTags() {
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of tagsViewStore.visitedViews) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
}
|
||||
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[0].fullPath ||
|
||||
(selectedTag.value as TagView).fullPath === "/index"
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLastView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
if(view.meta.externalUrl){
|
||||
tagsViewStore.delExternalViews(view);
|
||||
}
|
||||
tagsViewStore.delCachedView(view);
|
||||
const { fullPath } = view;
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[]) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
router.replace({ path: "/redirect/" })
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view: TagView) {
|
||||
if(view.meta.externalUrl){
|
||||
tagsViewStore.delExternalViews(view);
|
||||
}
|
||||
tagsViewStore.delView(view).then((res: any) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOtherTags() {
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllTags() {
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
toLastView(res.visitedViews);
|
||||
});
|
||||
}
|
||||
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
const menuMinWidth = 105;
|
||||
|
||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = proxy?.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (l > maxLeft) {
|
||||
left.value = maxLeft;
|
||||
} else {
|
||||
left.value = l;
|
||||
}
|
||||
|
||||
top.value = e.clientY - 60;
|
||||
tagMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
width: 100%;
|
||||
height: 58px;
|
||||
line-height: 58px;
|
||||
background-color: var(--el-bg-color);
|
||||
// border-bottom: 1px solid #F0F0F0;
|
||||
// box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||
position: relative;
|
||||
|
||||
.tags-item {
|
||||
// display: inline-block;
|
||||
padding: 10px 20px;
|
||||
// margin: 8px 0 0 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
// border: 1px solid var(--el-border-color-light);
|
||||
border-bottom: none;
|
||||
color: #222;
|
||||
font-family: PingFang SC-Semibold, PingFang SC;
|
||||
// font-weight: 600;
|
||||
// background-color: #FAFAFA;
|
||||
// border-radius: 12px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2467E8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: #2467E8;
|
||||
|
||||
}
|
||||
|
||||
&-close {
|
||||
display: inline-block;
|
||||
border-radius: 100%;
|
||||
transform: scale(1.2);
|
||||
|
||||
&:hover {
|
||||
color: #2467E8;
|
||||
background: rgb(0 0 0 / 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tag-menu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
line-height: 26px !important;
|
||||
|
||||
li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<el-container style="height: 100%;background-color: #071635;">
|
||||
<el-header :class="{ 'fixed-header': true }"
|
||||
:style="`background-color:${settingsStore.currentLayoutColor.navrbarBackgroundColor}`" height="64px">
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-container class="app-main-content-container" :style="{ marginTop: fixedHeader ? '64px' : 0 }">
|
||||
<Transition name="aside">
|
||||
<el-aside v-show="appStore.sidebar.moduleNumber>1" :width="appStore.sidebar.opened ? sideBarConfig.sideBarWidth : sideBarConfig.sideBarHideWidth">
|
||||
<Sidebar />
|
||||
</el-aside>
|
||||
</Transition>
|
||||
<el-main class="main-wrapper" style="overflow:inherit;width: 80%;">
|
||||
<!-- <div :class="{ 'fixed-tags': fixedTags && showTagsView }" :style="{width: fixedTags && showTagsView ? `calc(100% - ${appStore.sidebar.opened ? '248px' : '96px'})` : ''}"> -->
|
||||
<div :class="{ 'fixed-tags': fixedTags && showTagsView }" :style="{width: tagsViewWidth}">
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
<app-main style="width: 100%;" />
|
||||
<el-backtop target=".app-main-content-container .main-wrapper" :right="30" :bottom="5" :visibility-height="50" />
|
||||
</el-main>
|
||||
</el-container>
|
||||
<WebsiteInfo></WebsiteInfo>
|
||||
</el-container>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import sideBarConfig from '@/framework/styles/variables.module.scss'
|
||||
import Header from "./components/Header/index.vue";
|
||||
import Sidebar from "./components/Sidebar/index.vue";
|
||||
import AppMain from "./components/AppMain.vue";
|
||||
import TagsView from "./components/TagsView/index.vue";
|
||||
import WebsiteInfo from "@/framework/layoutpublic/WebsiteInfo/index.vue"
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const fixedTags = computed(() => settingsStore.fixedTags);
|
||||
|
||||
import { useAppStore } from "@/framework/store/modules/app";
|
||||
const appStore = useAppStore()
|
||||
|
||||
const tagsViewWidth = computed(() => {
|
||||
if(fixedTags && showTagsView){
|
||||
if(appStore.sidebar.opened){
|
||||
if(appStore.sidebar.moduleNumber>1){
|
||||
return 'calc(100% - 248px)'
|
||||
}
|
||||
return 'calc(100% - 32px)'
|
||||
}
|
||||
return 'calc(100% - 96px)'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.fixed-tags {
|
||||
// width: calc(100% - $sideBarWidth - 32px);
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
z-index: 100;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
// height: calc(100vh - 64px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-main-content-container{
|
||||
padding: 16px 12px;
|
||||
background-color: #F4f4f4;
|
||||
|
||||
display: flex;
|
||||
|
||||
.main-wrapper{
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
background-color: #F4f4f4;
|
||||
}
|
||||
}
|
||||
|
||||
.aside-enter-active,
|
||||
.aside-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.aside-enter-from,
|
||||
.aside-leave-to {
|
||||
transform: translateX(-100px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<!--搜索条件-->
|
||||
<div class="from-container" >
|
||||
<el-form ref="queryFromRef" label-width="82px" :model="queryFrom" >
|
||||
<el-row :gutter="0">
|
||||
<el-col :span="7">
|
||||
<el-form-item label="创建时间" prop="crtTime">
|
||||
<el-date-picker v-model="queryFrom.crtTime" type="daterange" unlink-panels range-separator="-"
|
||||
start-placeholder="开始时间" end-placeholder="结束时间"
|
||||
:default-value="[XEUtils.getWhatMonth(new Date(), 0, 'first'), new Date()]" :clearable="false"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-form-item label="状态" prop="state" >
|
||||
<el-select v-model="queryFrom.state" placeholder="请选择状态" clearable>
|
||||
<el-option
|
||||
v-for="item in stateOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="9" class="ab-text-right" >
|
||||
<el-button type="primary" @click="useTableQueryReload(tableRef)">查询</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<!--表格-->
|
||||
<div class="table-container">
|
||||
<vxe-grid ref="tableRef" v-bind="tableOptions">
|
||||
<template #module="{ row }">
|
||||
{{ row.menu }}/{{ row.module }}
|
||||
</template>
|
||||
<template #state="{ row }">
|
||||
<el-text v-if="1===row.state" type="primary">处理中</el-text>
|
||||
<el-text v-else-if="2===row.state" type="success">成功</el-text>
|
||||
<el-text v-else-if="-1===row.state" type="danger">失败</el-text>
|
||||
<el-text v-else>待处理</el-text>
|
||||
</template>
|
||||
<template #progress="{ row }">
|
||||
<el-text v-if="row.progress">{{row.progress}}%</el-text>
|
||||
<el-text v-else></el-text>
|
||||
</template>
|
||||
<template #operate="{ row }">
|
||||
<el-link v-if="row.address" type="primary" :underline="false" @click="downloadFile(row.address)" title="下载"><el-icon size="20"><Download /></el-icon></el-link>
|
||||
<el-text v-else></el-text>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import XEUtils from 'xe-utils'
|
||||
import { useQueryParamsHandle,useTableQueryReload } from '@/framework/hooks/useVxeTableHandle'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {queryAsyncEvents} from '@/framework/api/sms/index'
|
||||
import { gridDefaultProps } from '@/framework/utils/standard/vxe.ts'
|
||||
import { useUserStore } from "@/framework/store/modules/user";
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const stateOptions = [{ value: '0', label: '待处理', }, { value: '1', label: '处理中', }, { value: '2', label: '成功', }, { value: '-1', label: '失败', }]
|
||||
|
||||
const downloadFile = (address) => {
|
||||
if(XEUtils.startsWith(address, '/')){
|
||||
window.location.href=address
|
||||
}else{
|
||||
window.location.href='/'+address
|
||||
}
|
||||
}
|
||||
|
||||
const queryFromRef = ref<FormInstance>()
|
||||
const queryFrom=reactive({ state:'', crtTime:ref<[Date, Date]>([XEUtils.getWhatMonth(new Date(), 0, 'first'),new Date(),]) })
|
||||
/**表格初始化 */
|
||||
// const tableData = ref([])
|
||||
const tableRef = ref<VxeGridInstance>()//表格对象
|
||||
const tableOptions = reactive<VxeGridProps>(gridDefaultProps({
|
||||
params: reactive<VxeTableParam>({
|
||||
clientPage: false, //是否客户端分页
|
||||
}),
|
||||
rowConfig: {keyField: 'id'},//自定义行数据唯一主键的字段名
|
||||
pagerConfig: { enabled: true, pageSize: 10, pageSizes: [10, 20, 50, 100] },//分页配置
|
||||
proxyConfig: {
|
||||
seq: true, // 启用动态序号代理,每一页的序号会根据当前页数变化
|
||||
sort: true, // 启用排序代理,当点击排序时会自动触发 query 行为
|
||||
filter: true, // 启用筛选代理,当点击筛选时会自动触发 query 行为
|
||||
props: { result: 'data', total: 'total' },// 对应响应结果,按照后台服务PageResult对象配置
|
||||
ajax: {
|
||||
// 接收 Promise 对象
|
||||
query: ({ page, sorts, filters }) => {
|
||||
const params={state:queryFrom.state,startdate:'',enddate:''}
|
||||
if(''!==queryFrom.crtTime){
|
||||
params.startdate=XEUtils.toDateString(queryFrom.crtTime[0], 'yyyy-MM-dd')
|
||||
params.enddate=XEUtils.toDateString(queryFrom.crtTime[1], 'yyyy-MM-dd')
|
||||
}
|
||||
const queryParams = useQueryParamsHandle(params, page, sorts, filters)//请求参数处理
|
||||
return queryAsyncEvents(queryParams) //服务端分页查询
|
||||
},
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号',width:'50px',align:'center'},
|
||||
{ field: 'businessName', title: '业务类型' },
|
||||
{ field: 'businessCode', title: '业务码' },
|
||||
// { field: 'module', title: '模块',slots: {default: 'module'},},
|
||||
{ field: 'state', title: '状态',align:'center', width:'80px',slots: {default: 'state'},},
|
||||
{ field: 'progress', title: '进度',align:'center', width:'80px',slots: {default: 'progress'},},
|
||||
{ field: 'crtTime',title: '创建时间' ,align:'center', width:'160px'},
|
||||
{ field: 'reason', title: '失败原因' },
|
||||
{ field: '', title: '操作',slots: {default: 'operate'},align:'center'},//操作类居中显示
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<div class="userinfo-container" @mouseover="showDropDown = true" @mouseleave="onLeave">
|
||||
|
||||
<!-- 登录人头像-->
|
||||
<el-avatar :size="24" :src="userStore.avatarUrl" />
|
||||
<div class="ab-m-l-6">
|
||||
{{ userStore.userName }}
|
||||
<!-- {{ ''===userStore.tenantName||null===userStore.tenantName||undefined===userStore.tenantName?'':'['+userStore.tenantName+']' }}{{ userStore.userName }} -->
|
||||
</div>
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
<Transition name="userdropdown">
|
||||
<div v-show="showDropDown" class="menu-list-wrap" @mouseover="onEnter" @mouseleave="showDropDown = false">
|
||||
<div class="empty"> </div>
|
||||
|
||||
<div class="menu-list">
|
||||
<div class="top">
|
||||
<el-avatar :size="48" :src="userStore.avatarUrl" />
|
||||
<div class="ab-m-l-8">
|
||||
<div class="name"> {{ userStore.userName }}</div>
|
||||
<div class="dept"> {{ userStore.tenantName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item area-menu" v-if="switchBusinessDomainEnable" @mouseover="showUserBD = true" @mouseleave="onBDLeave">
|
||||
<div class="item-content">
|
||||
<img :src="icUserSwitchPng" style="width: 28px;height: 28px;" />
|
||||
<span>切换业务域</span>
|
||||
</div>
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
<Transition name="businessdomain">
|
||||
<div class="area-list-wrap" v-show="showUserBD" @mouseover="onBDEnter" @mouseleave="showUserBD = false">
|
||||
<div v-for="bd of businessDomain" :key="bd.id" >
|
||||
<div class="area-item-choose" v-if="bd.id===currentBusinessDomain.id">
|
||||
<img :src="icUserBssPng" style="width: 28px;height: 28px;" />
|
||||
<span>[{{ bd.id }}]{{ bd.name }}</span>
|
||||
</div>
|
||||
<div class="area-item" v-else @click="confirmBusinessDomain(bd)">
|
||||
<img :src="icUserBssPng" style="width: 28px;height: 28px;" />
|
||||
<span>[{{ bd.id }}]{{ bd.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div v-if="settingsStore.showPersonalCenter" class="item" @click="personalCenterShow=true">
|
||||
<div class="item-content">
|
||||
<img :src="icUserUsercenterPng" style="width: 28px;height: 28px;" />
|
||||
<span>个人中心</span>
|
||||
</div>
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</div>
|
||||
<div v-if="settingsStore.showDownloadCenter" class="item" @click="downloadCenterShow=true">
|
||||
<div class="item-content">
|
||||
<img :src="icUserFilesPng" style="width: 28px;height: 28px;" />
|
||||
<span>文件中心</span>
|
||||
</div>
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</div>
|
||||
<div class="item" @click="logout">
|
||||
<div class="item-content">
|
||||
<img :src="icUserlogoutPng" style="width: 28px;height: 28px;" />
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="sbdDialogVisible" title="切换业务域" width="30%" >
|
||||
<div style="display: flex;justify-content: center; align-items: center;">
|
||||
<el-text type="primary" size="large">[{{ tempBusinessDomain.id }}]{{ tempBusinessDomain.name }}</el-text>
|
||||
</div>
|
||||
<div><el-text type="warning">提示:确认切换后,将关闭其他标签页,并刷新当前页</el-text></div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="sbdDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="switchBusinessDomain()">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!--个人中心-->
|
||||
<el-drawer v-model="personalCenterShow" append-to-body direction="rtl" size="85%" title="个人中心">
|
||||
<PersonalCenter v-if="personalCenterShow"></PersonalCenter>
|
||||
</el-drawer>
|
||||
<!--文件中心-->
|
||||
<el-drawer v-model="downloadCenterShow" append-to-body direction="rtl" size="85%" title="文件中心">
|
||||
<DownloadCenter v-if="downloadCenterShow"></DownloadCenter>
|
||||
</el-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PersonalCenter from "@/framework/layoutpublic/PersonalCenter/index.vue"
|
||||
import DownloadCenter from "@/framework/layoutpublic/DownloadCenter/index.vue"
|
||||
import icUserSwitchPng from '@/assets/img/icon/ic_user_switch.png'
|
||||
import icUserBssPng from '@/assets/img/icon/ic_user_bss.png'
|
||||
import icUserUsercenterPng from '@/assets/img/icon/ic_user_usercenter.png'
|
||||
import icUserdownloadcenterPng from '@/assets/img/icon/ic_user_downloadcenter.png'
|
||||
import icUserFilesPng from '@/assets/img/icon/ic_user_files.png'
|
||||
import icUserlogoutPng from '@/assets/img/icon/ic_user_logout.png'
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import { useUserStore,setCurrentBusinessDomain,getCurrentBusinessDomain } from "@/framework/store/modules/user";
|
||||
import { getBusinessDomain } from '@/framework/api/acas/index'
|
||||
import { useTagsViewStore } from "@/framework/store/modules/tagsView";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const showDropDown=ref(false)
|
||||
let timer:number = 0
|
||||
const onEnter = ()=>{
|
||||
clearTimeout(timer)
|
||||
}
|
||||
const onLeave = ()=>{
|
||||
timer = window.setTimeout(()=>{
|
||||
showDropDown.value = false
|
||||
},200)
|
||||
}
|
||||
|
||||
const showUserBD=ref(false)
|
||||
let bdtimer:number = 0
|
||||
const onBDEnter = ()=>{
|
||||
clearTimeout(bdtimer)
|
||||
}
|
||||
const onBDLeave = ()=>{
|
||||
bdtimer = window.setTimeout(()=>{
|
||||
showUserBD.value = false
|
||||
},200)
|
||||
}
|
||||
|
||||
const bgColor=ref(settingsStore.currentLayoutColor.userBackgroundColor)
|
||||
const bgChooseColor=ref(settingsStore.currentLayoutColor.userChooseColor)
|
||||
|
||||
|
||||
const switchBusinessDomainEnable=ref(false)//切换业务域功能开关
|
||||
const businessDomain=ref([])//业务域列表
|
||||
const currentBusinessDomain=ref({})//当前业务域
|
||||
|
||||
const personalCenterShow=ref(false)//显示个人中心页面
|
||||
const downloadCenterShow=ref(false)//显示下载中心页面
|
||||
|
||||
|
||||
|
||||
/**退出登录 */
|
||||
const logout = () =>{
|
||||
userStore.logout()
|
||||
}
|
||||
|
||||
/**加载业务域 */
|
||||
const loadBusinessDomain = () => {
|
||||
getBusinessDomain().then((data: any) => {
|
||||
if(data.length>0){
|
||||
switchBusinessDomainEnable.value=true
|
||||
businessDomain.value=data
|
||||
for (const bd of businessDomain.value) {
|
||||
if(bd.defstatus&&1===bd.defstatus&&!currentBusinessDomain.value.id)
|
||||
currentBusinessDomain.value=setCurrentBusinessDomain(bd)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
/**切换业务域 */
|
||||
const router = useRouter();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const sbdDialogVisible =ref(false)
|
||||
const tempBusinessDomain =ref({})
|
||||
//确认切换业务域
|
||||
const confirmBusinessDomain = (row:Object) => {
|
||||
sbdDialogVisible.value=true
|
||||
tempBusinessDomain.value=row
|
||||
}
|
||||
//切换业务域
|
||||
const switchBusinessDomain = () => {
|
||||
if(undefined===tempBusinessDomain.value.id){
|
||||
ElMessage.error('业务域不存在');
|
||||
return false
|
||||
}
|
||||
currentBusinessDomain.value=setCurrentBusinessDomain(tempBusinessDomain.value)
|
||||
sbdDialogVisible.value = false
|
||||
tempBusinessDomain.value={}
|
||||
/**清空路由缓存,并刷新当前标签页 */
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
router.replace({ path: "/redirect"+router.currentRoute.value.fullPath })
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
currentBusinessDomain.value=getCurrentBusinessDomain()
|
||||
loadBusinessDomain()
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.userinfo-container{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: v-bind(bgColor);
|
||||
border-radius: 30px;
|
||||
padding: 10px 10px;
|
||||
cursor: pointer;
|
||||
position: reactive;
|
||||
|
||||
.menu-list-wrap{
|
||||
// display: none;
|
||||
width: 240px;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
|
||||
.empty{
|
||||
height: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.menu-list{
|
||||
background-color: v-bind(bgColor);
|
||||
border-radius: 16px;
|
||||
padding-bottom: 10px;
|
||||
user-select: none;
|
||||
.top{
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
|
||||
.dept{
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.item{
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px 0 4px;
|
||||
// margin: 0 4px;
|
||||
|
||||
.item-content{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img{
|
||||
margin-left: 14px;
|
||||
margin-right: 12px
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:hover{
|
||||
background: v-bind(bgChooseColor);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.area-menu{
|
||||
position: reactive;
|
||||
|
||||
.area-list-wrap{
|
||||
// display: none;
|
||||
width: 240px;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right:240px;
|
||||
z-index: 9999;
|
||||
background-color: v-bind(bgColor);
|
||||
border-radius: 16px;
|
||||
padding: 10px 4px;
|
||||
|
||||
.area-item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
|
||||
img{
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: v-bind(bgChooseColor);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.area-item-choose{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
background: v-bind(bgChooseColor);
|
||||
border-radius: 8px;
|
||||
|
||||
img{
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&:hover .area-list-wrap{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .menu-list-wrap{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.businessdomain-enter-active,
|
||||
.businessdomain-leave-active,
|
||||
.userdropdown-enter-active,
|
||||
.userdropdown-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.userdropdown-enter-from,
|
||||
.userdropdown-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
.businessdomain-enter-from,
|
||||
.businessdomain-leave-to {
|
||||
transform: translateX(5px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="ab-m-l-20">
|
||||
<div class="ab-p-b-20" style="display: flex;align-items: right;align-items:center;border-bottom: 1px solid #ece9e9;">
|
||||
<div>
|
||||
<el-avatar :size="80" :src="userStore.avatarUrl" @click="uploadAvatarShow=true"/>
|
||||
</div>
|
||||
<div style="padding-left: 10px;">
|
||||
<el-text tag="b" style="font-size: 16px;">{{employee.name}}</el-text>
|
||||
<br/>
|
||||
<el-tag ><UserFilled width="14"/>{{employee.positionName}}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid #ece9e9;" class="ab-p-b-20 ab-m-t-20">
|
||||
<AbSection title="基础信息" :showLine="false"></AbSection>
|
||||
<el-row >
|
||||
<el-col class="ab-m-t-10" :span="12"><el-text size="large">人员ID </el-text><el-text size="large" class="ab-m-l-30">{{employee.id}}</el-text></el-col>
|
||||
<el-col class="ab-m-t-10" :span="12" v-if="employee.personnelId"><el-text size="large">人员编码</el-text><el-text size="large" class="ab-m-l-30">{{employee.personnelId}}</el-text></el-col>
|
||||
<el-col class="ab-m-t-10" :span="12"><el-text size="large">手机号 </el-text><el-text size="large" class="ab-m-l-30" >{{employee.mobile}}</el-text></el-col>
|
||||
<el-col class="ab-m-t-10" :span="12"><el-text size="large">所属部门</el-text><el-text size="large" class="ab-m-l-30">[{{employee.dept?.id}}]{{employee.dept?.name}}</el-text></el-col>
|
||||
<el-col class="ab-m-t-10" :span="12" v-if="employee.hrstatus">
|
||||
<el-text size="large">人资状态</el-text>
|
||||
<el-text size="large" class="ab-m-l-30" v-if="0===employee.hrstatus">正式</el-text>
|
||||
<el-text size="large" class="ab-m-l-30" v-else-if="1===employee.hrstatus">试用</el-text>
|
||||
<el-text size="large" class="ab-m-l-30" v-else>离职</el-text>
|
||||
</el-col>
|
||||
<el-col class="ab-m-t-10" :span="12">
|
||||
<el-text size="large">用户状态</el-text>
|
||||
<el-text size="large" class="ab-m-l-30" v-if="0===employee.status">启用</el-text>
|
||||
<el-text size="large" class="ab-m-l-30" v-else>停用</el-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div style="border-bottom: 1px solid #ece9e9;" class="ab-p-b-20 ab-m-t-20">
|
||||
<AbSection title="安全设置" :showLine="false"></AbSection>
|
||||
<div class="ab-m-t-10" style="display: flex;justify-content: space-between; align-items: right;align-items:center;">
|
||||
<div style="display: flex;align-items: right;align-items:center;">
|
||||
<div><el-text size="large">登录密码</el-text></div>
|
||||
<div class="ab-m-l-20">
|
||||
<el-text size="large" tag="b">已设置</el-text>
|
||||
<el-text size="large" type="info" class="ab-m-l-10">长度6-16位字符,必须同时包含数字和大小写字母,不允许有空格、中文。</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ab-m-r-20">
|
||||
<el-button type="primary" @click="updatepwdShow=true">修改</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 修改密码-->
|
||||
<el-dialog v-model="updatepwdShow" width="500" title="密码修改">
|
||||
<el-form v-loading="loginLoading" label-width="110px" ref="updatePwdRuleFormRef" :rules="updatePwdRules" :model="updatePwdForm" @keyup.enter.native="updatePwd(updatePwdRuleFormRef)" >
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-text >{{updatePwdForm.username}}</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item prop="newpassword" label="新密码">
|
||||
<el-tooltip effect="dark" placement="top" >
|
||||
<template #content> 密码长度6-16位字符<br />必须同时包含数字和大小写字母<br />不允许有空格、中文</template>
|
||||
<el-input type="password" v-model.trim="updatePwdForm.newpassword" placeholder="请输入新密码" autocomplete="off" show-password />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmnewpassword" label="确认新密码">
|
||||
<el-input type="password" v-model.trim="updatePwdForm.confirmnewpassword" placeholder="请再次输入新密码" autocomplete="off" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span style="text-align: right;width: 100%;">
|
||||
<el-button @click="updatepwdShow=false" >取消</el-button>
|
||||
<el-button type="primary" @click="updatePwd(updatePwdRuleFormRef)" >确定</el-button>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 上传头像-->
|
||||
<el-dialog v-model="uploadAvatarShow" width="400" title="上传头像">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-upload
|
||||
ref="avatarUpload"
|
||||
name="image_upload"
|
||||
class="avatar-uploader"
|
||||
action="/acas/employee/uploadPhoto"
|
||||
:auto-upload="true"
|
||||
:headers="avatarHeaders"
|
||||
:show-file-list="false"
|
||||
list-type="picture-card"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:on-error="handleAvatarError"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-text type="danger" size="large" >注意事项</el-text>
|
||||
<el-row style="margin-top: 10px;" v-for="(value,index) in avatarTypeTips" :key="value">
|
||||
<el-col :span="24"><el-text type="danger" >{{index+1}}、{{value}}</el-text></el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AbSection from '@/framework/components/standard/AbSection/index.vue'
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getEmployee,baseLoginUpdatePwd,getKey } from '@/framework/api/acas/index'
|
||||
import { useUserStore,getToken,getTokenHeader } from "@/framework/store/modules/user";
|
||||
import sm from 'sm-crypto'
|
||||
const userStore = useUserStore()
|
||||
const employee=ref({})
|
||||
const settingsStore = useSettingsStore()
|
||||
const cToken=getToken()
|
||||
const currentLoginType=ref(settingsStore.loginType.find((o: any) => o.id === cToken.user_type))
|
||||
|
||||
//修改密码表单
|
||||
const loginLoading=ref(false)
|
||||
const updatepwdShow=ref(false)
|
||||
const updatePwdForm = reactive({username: '',newpassword: '',confirmnewpassword: '',})
|
||||
const updatePwdRuleFormRef = ref<FormInstance>()
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (value !== updatePwdForm.newpassword) {
|
||||
callback(new Error("两次输入密码不一致"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const updatePwdRules = reactive<FormRules>({
|
||||
newpassword: [{ required: true, message: '新密码不能为空', trigger: 'blur' },],
|
||||
confirmnewpassword: [{ required: true, message: '请再次输入新密码', trigger: 'blur' },{ validator: validateConfirmPassword, trigger: 'blur' }],
|
||||
})
|
||||
/**修改密码 */
|
||||
const updatePwd = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !await formEl.validate((valid, fields) => { return valid }))
|
||||
return
|
||||
let prefixUrl=currentLoginType.value.prefixUrl
|
||||
if(''===prefixUrl){
|
||||
ElMessage({type: 'error',message: '['+currentLoginType.value.name+']配置有误' })
|
||||
return;
|
||||
}
|
||||
let k=await getKey()
|
||||
let npe=sm.sm2.doEncrypt(updatePwdForm.newpassword, k);
|
||||
let cnpe=sm.sm2.doEncrypt(updatePwdForm.confirmnewpassword, k);
|
||||
loginLoading.value=true
|
||||
|
||||
|
||||
baseLoginUpdatePwd(prefixUrl+'/loginChangePassword',{username: updatePwdForm.username,newpassword: npe,confirmnewpassword: cnpe,}).then((data: any) => {
|
||||
updatePwdForm.newpassword=''
|
||||
updatePwdForm.confirmnewpassword=''
|
||||
ElMessage({type: 'success',message: '修改密码成功' })
|
||||
updatepwdShow.value=false
|
||||
}).finally(() => {
|
||||
loginLoading.value=false
|
||||
})
|
||||
}
|
||||
/**上传头像 */
|
||||
const avatarUpload=ref()
|
||||
const avatarHeaders=getTokenHeader()
|
||||
const uploadAvatarShow=ref(false)
|
||||
const imageUrl = ref('')
|
||||
const avatarTypeTips=['头像图片文件支持jpeg、png格式!','头像图片文件大小不能超过500KB!']
|
||||
const handleAvatarSuccess: UploadProps['onSuccess'] = (response,uploadFile) => {
|
||||
if(200==response.status){
|
||||
userStore.avatarUrl=response.data
|
||||
uploadAvatarShow.value=false
|
||||
}else{
|
||||
ElMessage.error((response.errorCode?'['+response.errorCode+']':'')+'系统异常');
|
||||
console.error('系统异常', response);
|
||||
}
|
||||
}
|
||||
const handleAvatarError: UploadProps['onError'] = (response,uploadFile) => {
|
||||
console.error('系统异常', response);
|
||||
ElMessage.error('系统异常');
|
||||
}
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
console.log(rawFile)
|
||||
if (rawFile.type !== 'image/jpeg'&&rawFile.type !== 'image/png') {
|
||||
ElMessage.error(avatarTypeTips[0])
|
||||
return false
|
||||
} else if (rawFile.size / 1024 / 500 > 2) {
|
||||
ElMessage.error(avatarTypeTips[1])
|
||||
return false
|
||||
}
|
||||
imageUrl.value = URL.createObjectURL(rawFile!)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getEmployee({id:userStore.userId}).then((data: any) => {
|
||||
employee.value=data;
|
||||
updatePwdForm.username=employee.value.id
|
||||
if(employee.value.photoUrl){
|
||||
userStore.avatarUrl=employee.value.photoUrl
|
||||
}
|
||||
}).finally(() => {
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="system-menu" @mouseover="showSystem = true" @mouseleave="onLeave" v-if="settingsStore.showSubSystemTags">
|
||||
<div class="current-menu">
|
||||
<el-icon><Menu /></el-icon>
|
||||
<span class="ab-m-l-5">{{ currentSubsystem?.name }}</span>
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</div>
|
||||
<Transition name="subsystemilst">
|
||||
<div class="other-menu" v-show="showSystem&&allSubsystems.length>0" @mouseover="onEnter" @mouseleave="showSystem = false" :style="`background-color:${settingsStore.currentLayoutColor.navrbarBackgroundColor}`">
|
||||
<div class="item" v-for="item in allSubsystems" :key="item.id" @click="skipSubsystem(item)" :class="{active: currentSubsystem.id===item.id}" >{{ item.name }}</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import { getAllowAccessSubsystem } from '@/framework/api/acas/index'
|
||||
const settingsStore = useSettingsStore()
|
||||
const allSubsystems=ref([])
|
||||
const currentSubsystem=ref({id:publicData.currentSubsystem,name:publicData.currentSubsystem})
|
||||
const systemChooseColor=ref(settingsStore.currentLayoutColor.systemChooseColor)
|
||||
|
||||
/**加载子系统 */
|
||||
const loadAllowAccessSubsystem = () => {
|
||||
allSubsystems.value=[]
|
||||
getAllowAccessSubsystem().then((data: any) => {
|
||||
for(let subsystem of data){
|
||||
allSubsystems.value.push({id:subsystem.id,name:subsystem.name,describe:subsystem.describe,auth:true, indexURL:subsystem.indexURL,logo:''})
|
||||
}
|
||||
currentSubsystem.value=allSubsystems.value.find((item: any) => item.id === publicData.currentSubsystem)
|
||||
})
|
||||
}
|
||||
/**跳转子系统 */
|
||||
const skipSubsystem = (subsystem:any) => {
|
||||
if(currentSubsystem.value.id===subsystem.id) return
|
||||
window.open(location.origin+subsystem.indexURL+'?t='+new Date().getTime(),'_blank')
|
||||
}
|
||||
|
||||
const showSystem = ref(false)
|
||||
let timer:number = 0
|
||||
const onEnter = ()=>{
|
||||
clearTimeout(timer)
|
||||
}
|
||||
const onLeave = ()=>{
|
||||
timer = window.setTimeout(()=>{
|
||||
showSystem.value = false
|
||||
},200)
|
||||
}
|
||||
onMounted(() => {
|
||||
loadAllowAccessSubsystem()
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-menu{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.current-menu{
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.other-menu{
|
||||
width: 480px;
|
||||
height: auto;
|
||||
color: rgba(255,255,255,0.45);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 42px;
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
.item{
|
||||
width: 115px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 10px;
|
||||
&:hover{
|
||||
color: #fff;
|
||||
background-color: v-bind(systemChooseColor);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
.active{
|
||||
color: #fff;
|
||||
background-color: v-bind(systemChooseColor);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.subsystemilst-enter-active,
|
||||
.subsystemilst-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.subsystemilst-enter-from,
|
||||
.subsystemilst-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div v-if="settingsStore.websiteInfo.show" style="display: flex;flex-wrap: wrap;padding-top: 5px;background-color: #fff;" :style="{justifyContent: settingsStore.websiteInfo.justify }">
|
||||
<template v-for="(info, index) in settingsStore.websiteInfo.otherInfo">
|
||||
<span style="padding-top: 3px;">
|
||||
<el-divider v-if="index>0" direction="vertical" />
|
||||
<el-link :href=info.url target="_blank" type="info" :underline="false">{{ info.name }}</el-link>
|
||||
</span>
|
||||
</template>
|
||||
<span id="container_ideConac" style="margin-left: 10px;"></span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
/**加载党政机关事业单位网站标识 */
|
||||
const loadConac = () => {
|
||||
let conacUrl=settingsStore.websiteInfo.conacUrl.url;
|
||||
if(!conacUrl){
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById('container_ideConac')
|
||||
if(!container){
|
||||
return;
|
||||
}
|
||||
const span = document.createElement('span')
|
||||
span.id = '_ideConac'
|
||||
const script = document.createElement('script')
|
||||
script.src = conacUrl
|
||||
script.type = 'text/javascript'
|
||||
script.async = true
|
||||
container.appendChild(span)
|
||||
container.appendChild(script)
|
||||
|
||||
let count=0;
|
||||
let imgConacTimer=setInterval(function() {
|
||||
const img = document.getElementById('imgConac')
|
||||
if(img){
|
||||
img.style.width = settingsStore.websiteInfo.conacUrl.size?settingsStore.websiteInfo.conacUrl.size+'px':'30px';
|
||||
clearInterval(imgConacTimer);
|
||||
}
|
||||
count++;
|
||||
if(count==30){
|
||||
clearInterval(imgConacTimer);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if(!settingsStore.websiteInfo.show)
|
||||
return;
|
||||
loadConac();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { dynamicRouter } from "@/subsystem/router/dynamicRouter";
|
||||
import pinia from "@/framework/store";
|
||||
import {useSettingsStore } from "@/framework/store/modules/settings";
|
||||
|
||||
const layout=useSettingsStore(pinia).currentLayout
|
||||
|
||||
/**生成动态路由 */
|
||||
export const generateDynamicRouter = (menu:any) => {
|
||||
menu.component=layout
|
||||
menu.children.forEach(c => {
|
||||
//c.component=() => import('@/framework/views/ext/index.vue')
|
||||
});
|
||||
return menu
|
||||
}
|
||||
|
||||
/**获取子系统路由 */
|
||||
export const getSubSystemRouter = () => {
|
||||
dynamicRouter.forEach(o => {
|
||||
o.component=layout
|
||||
});
|
||||
return dynamicRouter
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// src/router/index.ts
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw,useRouter } from 'vue-router';
|
||||
import { staticRouter, errorRouter } from './staticRouter';
|
||||
|
||||
/**
|
||||
* 创建路由
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [ ...staticRouter, ...errorRouter] as RouteRecordRaw[],
|
||||
// 刷新时,滚动条位置还原
|
||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置路由
|
||||
*/
|
||||
export function resetRouter() {
|
||||
router.replace({ path: '/login' });
|
||||
location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前路由
|
||||
*/
|
||||
export function currentRouter() {
|
||||
return router.currentRoute
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import pinia from "@/framework/store";
|
||||
import {useSettingsStore } from "@/framework/store/modules/settings";
|
||||
|
||||
const layout=useSettingsStore(pinia).currentLayout
|
||||
/**
|
||||
* staticRouter (静态路由)
|
||||
*/
|
||||
export const staticRouter: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/redirect',
|
||||
component: layout,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
component: () => import('@/framework/views/redirect/index.vue'),
|
||||
meta: { title: '重定向' },
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* errorRouter (错误页面路由)
|
||||
*/
|
||||
export const errorRouter = [
|
||||
{
|
||||
path: "/403",
|
||||
name: "403",
|
||||
component: () => import("@/framework/components/ErrorMessage/403.vue"),
|
||||
meta: {
|
||||
title: "403页面"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/404",
|
||||
name: "404",
|
||||
component: () => import("@/framework/components/ErrorMessage/404.vue"),
|
||||
meta: {
|
||||
title: "404页面"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/500",
|
||||
name: "500",
|
||||
component: () => import("@/framework/components/ErrorMessage/500.vue"),
|
||||
meta: {
|
||||
title: "500页面"
|
||||
}
|
||||
},
|
||||
// Resolve refresh page, route warnings
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
component: () => import("@/framework/components/ErrorMessage/404.vue")
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createPinia } from "pinia";
|
||||
// import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
|
||||
// pinia persist
|
||||
const pinia = createPinia();
|
||||
// pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
export default pinia;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { defineStore } from "pinia";
|
||||
// import { useStorage } from "@vueuse/core";
|
||||
// import defaultSettings from "@/settings";
|
||||
|
||||
// 导入 Element Plus 中英文语言包
|
||||
// import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
// import en from "element-plus/es/locale/lang/en";
|
||||
|
||||
// setup
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
// state
|
||||
// const device = useStorage("device", "desktop");
|
||||
// const size = useStorage<any>("size", defaultSettings.size);
|
||||
// const language = useStorage("language", defaultSettings.language);
|
||||
|
||||
// const sidebarStatus = useStorage("sidebarStatus", "closed");
|
||||
const sidebar = reactive({
|
||||
moduleNumber: 0,
|
||||
opened: true,
|
||||
withoutAnimation: false,
|
||||
});
|
||||
|
||||
// /**
|
||||
// * 根据语言标识读取对应的语言包
|
||||
// */
|
||||
// const locale = computed(() => {
|
||||
// if (language?.value == "en") {
|
||||
// return en;
|
||||
// } else {
|
||||
// return zhCn;
|
||||
// }
|
||||
// });
|
||||
|
||||
// actions
|
||||
function toggleSidebar(withoutAnimation: boolean) {
|
||||
sidebar.opened = !sidebar.opened;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
// if (sidebar.opened) {
|
||||
// sidebarStatus.value = "opened";
|
||||
// } else {
|
||||
// sidebarStatus.value = "closed";
|
||||
// }
|
||||
}
|
||||
|
||||
function closeSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = false;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
// sidebarStatus.value = "closed";
|
||||
}
|
||||
|
||||
function openSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = true;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
// sidebarStatus.value = "opened";
|
||||
}
|
||||
|
||||
// function toggleDevice(val: string) {
|
||||
// device.value = val;
|
||||
// }
|
||||
|
||||
// function changeSize(val: string) {
|
||||
// size.value = val;
|
||||
// }
|
||||
/**
|
||||
* 切换语言
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
// function changeLanguage(val: string) {
|
||||
// language.value = val;
|
||||
// }
|
||||
|
||||
return {
|
||||
// device,
|
||||
sidebar,
|
||||
// language,
|
||||
// locale,
|
||||
// size,
|
||||
// toggleDevice,
|
||||
// changeSize,
|
||||
// changeLanguage,
|
||||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { defineStore } from "pinia";
|
||||
import { getSubSystemRouter,generateDynamicRouter } from "@/framework/router/dynamicRouter";
|
||||
|
||||
import pinia from "@/framework/store";
|
||||
import { listRoutes,getBusinessDomain } from "@/framework/api/acas";
|
||||
import { Menu } from "@element-plus/icons-vue";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import { setCurrentBusinessDomain,getCurrentBusinessDomain } from "@/framework/store/modules/user";
|
||||
|
||||
interface Menu {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
modules: Menu[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配一级菜单路由
|
||||
* @param menu 一级菜单
|
||||
* @param menuRouter 一级菜单路由
|
||||
* @returns
|
||||
*/
|
||||
const matchMenu = (menu:Menu,menuRouter: RouteRecordRaw[]) => {
|
||||
const url = menu.url
|
||||
let temp = null
|
||||
for (const route of menuRouter) {
|
||||
if (url === route.path) {
|
||||
temp = route
|
||||
temp.meta.title = menu.name
|
||||
break
|
||||
}
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配二级菜单路由
|
||||
* @param module 二级菜单
|
||||
* @param moduleRouter 二级菜单路由
|
||||
* @returns
|
||||
*/
|
||||
const matchModule = (module:Menu,moduleRouter: RouteRecordRaw[]) => {
|
||||
let temp=null;
|
||||
for (const children of moduleRouter) {
|
||||
if (module.url === children.path) {
|
||||
children.meta.id = module.id
|
||||
children.meta.title = module.name
|
||||
temp=children;
|
||||
break
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一级菜单路由
|
||||
* @param params
|
||||
*/
|
||||
const createMenuRouter = (menu:Menu) => {
|
||||
if(undefined===menu.modules||menu.modules.length==0)
|
||||
return null
|
||||
let menuRouter= {
|
||||
path: '/'+menu.id,
|
||||
name: menu.id,
|
||||
// redirect: '/document/dataDictionary',
|
||||
meta: {title: menu.name},
|
||||
}
|
||||
let children=[]
|
||||
menu.modules.forEach(m => {
|
||||
let externalOldERP=false
|
||||
if('html'===m.url.substring(m.url.length-4,m.url.length))
|
||||
externalOldERP=true
|
||||
children.push({
|
||||
path: m.id,
|
||||
name: m.id,
|
||||
meta: {
|
||||
title: m.name,
|
||||
external: true, //是否引入外部应用,默认false
|
||||
externalOldERP: externalOldERP, //外部应用是否是老ERP页面,默认false
|
||||
externalOldERPIndex: publicData.externalOldERPIndex, //老ERP页面主页面,用于提前加载框架js、css等等
|
||||
externalUrl: m.url, //外部应用地址,如果是老ERP页面,地址会是:externalOldERPIndex+externalUrl
|
||||
}
|
||||
})
|
||||
});
|
||||
menuRouter.children=children
|
||||
menuRouter.redirect=menuRouter.path+'/'+menuRouter.children[0].path
|
||||
return generateDynamicRouter(menuRouter)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 递归过滤有权限的异步(动态)路由
|
||||
*
|
||||
* @param routes 接口返回的异步(动态)路由
|
||||
* @param roles 用户角色集合
|
||||
* @returns 返回用户有权限的异步(动态)路由
|
||||
*/
|
||||
const filterAsyncRoutes = (menus: Menu[], dynamicRouter: RouteRecordRaw[]) => {
|
||||
const asyncRoutes: RouteRecordRaw[] = [];
|
||||
menus.forEach(item => {
|
||||
let cmenu = matchMenu(item,dynamicRouter)
|
||||
if (cmenu) {
|
||||
const children = []
|
||||
for (const module of item.modules) {
|
||||
let cmodule=matchModule(module,cmenu.children)
|
||||
if(cmodule){
|
||||
children.push(cmodule)
|
||||
}
|
||||
}
|
||||
cmenu.children = children
|
||||
if (cmenu.children.length>0) {
|
||||
cmenu.redirect = cmenu.path+'/'+children[0].path
|
||||
asyncRoutes.push(cmenu)
|
||||
}
|
||||
}else{
|
||||
let newmenu=createMenuRouter(item)
|
||||
if(null!=newmenu)
|
||||
asyncRoutes.push(newmenu)
|
||||
}
|
||||
})
|
||||
const route = dynamicRouter.filter(val => val.path==='/')[0]
|
||||
route.redirect = asyncRoutes[0] ? asyncRoutes[0].redirect : '/404'
|
||||
asyncRoutes.unshift(route);
|
||||
return asyncRoutes;
|
||||
};
|
||||
|
||||
// setup
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
// state
|
||||
const routes = ref<RouteRecordRaw[]>([]);
|
||||
|
||||
// actions
|
||||
function setRoutes(newRoutes: RouteRecordRaw[]) {
|
||||
routes.value = getSubSystemRouter().concat(newRoutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并设置当前业务域
|
||||
* @returns
|
||||
*/
|
||||
function setAndGetBusinessDomain() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getBusinessDomain().then((data: any) => {
|
||||
if(data.length>0){
|
||||
let cbd=getCurrentBusinessDomain();
|
||||
for (const bd of data) {
|
||||
if(bd.defstatus&&1===bd.defstatus&&!cbd.id)
|
||||
setCurrentBusinessDomain(bd)
|
||||
}
|
||||
}
|
||||
resolve(data);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成动态路由
|
||||
*
|
||||
* @param roles 用户角色集合
|
||||
* @returns
|
||||
*/
|
||||
function generateRoutes(dynamicRouter: RouteRecordRaw[]) {
|
||||
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
|
||||
//使用外部本地菜单配置
|
||||
if(publicData.extLocalRouter&&publicData.extLocalRouter.length>0){
|
||||
const accessedRoutes = filterAsyncRoutes(publicData.extLocalRouter, dynamicRouter);
|
||||
setRoutes(accessedRoutes);
|
||||
resolve(accessedRoutes);
|
||||
}else{ //接口获取授权路由
|
||||
let params={subsystem:publicData.currentSubsystem}
|
||||
const settingsStore = useSettingsStore(pinia);
|
||||
if(settingsStore.backendServices[publicData.currentSubsystem])
|
||||
params.subsystem=settingsStore.backendServices[publicData.currentSubsystem]
|
||||
listRoutes(params).then(data => {
|
||||
const menus = data.filter((item: Menu) => item.modules.length>0)
|
||||
// 根据角色获取有访问权限的路由
|
||||
const accessedRoutes = filterAsyncRoutes(menus, dynamicRouter);
|
||||
setRoutes(accessedRoutes);
|
||||
resolve(accessedRoutes);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
routes.value = getSubSystemRouter()
|
||||
return { routes, setRoutes, generateRoutes,setAndGetBusinessDomain };
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function usePermissionStoreHook() {
|
||||
return usePermissionStore(pinia);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
|
||||
|
||||
export const useRoutesStore = defineStore('routes', () => {
|
||||
const hasRoutes = ref(false)
|
||||
const routes = reactive([])
|
||||
|
||||
// const getSubRoutes = computed((path: string) => routes.find((item: any) => {
|
||||
// console.log('哈哈', path);
|
||||
|
||||
// if (item.path === path) {
|
||||
// return item.children
|
||||
// }
|
||||
// }));
|
||||
|
||||
return { hasRoutes, routes }
|
||||
})
|
||||
@@ -0,0 +1,201 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { getAssetsImages } from '@/framework/utils/index'
|
||||
export const Layout = () => import('@/framework/layout/index.vue');//顶部菜单布局
|
||||
export const Layout1 = () => import('@/framework/layout1/index.vue');//顶部一级菜单、左侧二级菜单局部
|
||||
|
||||
declare var window: any
|
||||
/**
|
||||
* 菜单配色
|
||||
* navrbarBackgroundColor(顶部导航栏背景颜色)
|
||||
* menuFontTextColor(默认菜单名称颜色)
|
||||
* activeMenuFontTextColor(活动菜单名称颜色)#2467e8
|
||||
*
|
||||
* */
|
||||
|
||||
const layoutColorBlue={navrbarBackgroundColor:'#428bca',menuFontTextColor:'#ffffff',activeMenuFontTextColor:'#ffffff',activeMenuBackgroundColor:'#5FA8E7',
|
||||
userBackgroundColor:'#3399CC',userChooseColor:'#5FA8E7',systemChooseColor:'#5FA8E7',}
|
||||
const layoutColorDarkBlue={navrbarBackgroundColor:'#071635',menuFontTextColor:'#ffffff',activeMenuFontTextColor:'#4D7FE0',activeMenuBackgroundColor:'',
|
||||
userBackgroundColor:'#10234A',userChooseColor:'rgba(36,103,232,0.2)',systemChooseColor:'#0D2758'}
|
||||
//菜单布局处理
|
||||
const layoutHandle = (realSetting:object,currentLayout:string) => {
|
||||
if('layout'===currentLayout){
|
||||
realSetting.currentLayout = Layout
|
||||
}
|
||||
}
|
||||
//菜单配色处理
|
||||
const layoutColorHandle = (realSetting:object,currentLayoutColor:string) => {
|
||||
if('DarkBlue'===currentLayoutColor){
|
||||
realSetting.currentLayoutColor.value = layoutColorDarkBlue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**获取自定义配置 */
|
||||
const getCustConfig = (realSetting:object) => {
|
||||
if(!publicData.extConfigName) return;
|
||||
let extConfig=window[publicData.extConfigName]
|
||||
if(!extConfig||typeof extConfig !== "object") return;
|
||||
for (let key in realSetting) {
|
||||
if(extConfig[key]!==undefined){
|
||||
if('currentLayout'===key){
|
||||
layoutHandle(realSetting,extConfig[key])
|
||||
}else if('currentLayoutColor'===key){
|
||||
layoutColorHandle(realSetting,extConfig[key])
|
||||
}else
|
||||
realSetting[key].value =extConfig[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
/**获取默认配置 */
|
||||
const getDefaultConfig = (realSetting:object) => {
|
||||
if (window.frameBaseConfig) {
|
||||
if(window.frameBaseConfig.tokenKey!==undefined){
|
||||
realSetting.tokenKey.value =window.frameBaseConfig.tokenKey
|
||||
}
|
||||
if(window.frameBaseConfig.loginResultKey!==undefined){
|
||||
realSetting.loginResultKey.value =window.frameBaseConfig.loginResultKey
|
||||
}
|
||||
if(window.frameBaseConfig.currentLayout!==undefined){
|
||||
if('layout'===window.frameBaseConfig.currentLayout){
|
||||
realSetting.currentLayout = Layout
|
||||
}
|
||||
}
|
||||
if(window.frameBaseConfig.currentLayoutColor!==undefined){
|
||||
if('DarkBlue'===window.frameBaseConfig.currentLayoutColor){
|
||||
realSetting.currentLayoutColor.value = layoutColorDarkBlue
|
||||
}
|
||||
}
|
||||
|
||||
if(window.frameBaseConfig.backendServices!==undefined){
|
||||
realSetting.backendServices.value =window.frameBaseConfig.backendServices
|
||||
}
|
||||
if(window.frameBaseConfig.logoutRemoveToken!==undefined){
|
||||
realSetting.logoutRemoveToken.value =window.frameBaseConfig.logoutRemoveToken
|
||||
}
|
||||
if(window.frameBaseConfig.loginAddress!==undefined){
|
||||
realSetting.loginAddress.value =window.frameBaseConfig.loginAddress
|
||||
}
|
||||
if(window.frameBaseConfig.skipLoginAddressTime!==undefined){
|
||||
realSetting.skipLoginAddressTime.value =window.frameBaseConfig.skipLoginAddressTime
|
||||
}
|
||||
if(window.frameBaseConfig.loginType!==undefined){
|
||||
realSetting.loginType.value =window.frameBaseConfig.loginType
|
||||
}
|
||||
if(window.frameBaseConfig.navrbarBackgroundColor!==undefined){
|
||||
realSetting.navrbarBackgroundColor.value =window.frameBaseConfig.navrbarBackgroundColor
|
||||
}
|
||||
if(window.frameBaseConfig.menuFontTextColor!==undefined){
|
||||
realSetting.menuFontTextColor.value =window.frameBaseConfig.menuFontTextColor
|
||||
}
|
||||
if(window.frameBaseConfig.activeMenuFontTextColor!==undefined){
|
||||
realSetting.activeMenuFontTextColor.value =window.frameBaseConfig.activeMenuFontTextColor
|
||||
}
|
||||
if(window.frameBaseConfig.showLogo!==undefined){
|
||||
realSetting.showLogo.value =window.frameBaseConfig.showLogo
|
||||
}
|
||||
if(window.frameBaseConfig.logoAddress!==undefined){
|
||||
realSetting.logoAddress.value =window.frameBaseConfig.logoAddress
|
||||
}
|
||||
if(window.frameBaseConfig.headAddress!==undefined){
|
||||
realSetting.headAddress.value =window.frameBaseConfig.headAddress
|
||||
}
|
||||
if(window.frameBaseConfig.showSubSystemTags!==undefined){
|
||||
realSetting.showSubSystemTags.value =window.frameBaseConfig.showSubSystemTags
|
||||
}
|
||||
if(window.frameBaseConfig.showPersonalCenter!==undefined){
|
||||
realSetting.showPersonalCenter.value =window.frameBaseConfig.showPersonalCenter
|
||||
}
|
||||
if(window.frameBaseConfig.showDownloadCenter!==undefined){
|
||||
realSetting.showDownloadCenter.value =window.frameBaseConfig.showDownloadCenter
|
||||
}
|
||||
if(window.frameBaseConfig.showMessageNotify!==undefined){
|
||||
realSetting.showMessageNotify.value =window.frameBaseConfig.showMessageNotify
|
||||
}
|
||||
if(window.frameBaseConfig.tagsView!==undefined){
|
||||
realSetting.tagsView.value =window.frameBaseConfig.tagsView
|
||||
}
|
||||
if(window.frameBaseConfig.fixedHeader!==undefined){
|
||||
realSetting.fixedHeader.value =window.frameBaseConfig.fixedHeader
|
||||
}
|
||||
if(window.frameBaseConfig.fixedTags!==undefined){
|
||||
realSetting.fixedTags.value =window.frameBaseConfig.fixedTags
|
||||
}
|
||||
if(window.frameBaseConfig.maxTags!==undefined){
|
||||
realSetting.maxTags.value =window.frameBaseConfig.maxTags
|
||||
}
|
||||
if(window.frameBaseConfig.extModuleSendToken!==undefined){
|
||||
realSetting.extModuleSendToken.value =window.frameBaseConfig.extModuleSendToken
|
||||
}
|
||||
if(window.frameBaseConfig.websiteInfo!==undefined){
|
||||
realSetting.websiteInfo.value =window.frameBaseConfig.websiteInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore("setting", () => {
|
||||
//令牌key
|
||||
const tokenKey=ref('abacus_emp_token')
|
||||
//登录信息key
|
||||
const loginResultKey=ref('abacus_login_result' )
|
||||
//菜单布局类型
|
||||
let currentLayout = Layout1
|
||||
//菜单配色类型 layoutColorDarkBlue
|
||||
const currentLayoutColor=ref(layoutColorBlue)
|
||||
//后台服务配置{"acas":"example"}
|
||||
const backendServices=ref({})
|
||||
//退出登录是否清理登录令牌
|
||||
const logoutRemoveToken = ref<boolean>(false);
|
||||
//应用登录地址
|
||||
const loginAddress = ref<string>('/abacus/index.html');
|
||||
//跳转登录页面延迟时间
|
||||
const skipLoginAddressTime = ref<number>(3000);
|
||||
//登录类型相关配置,acas员工登录默认放第一个
|
||||
const loginType = ref([{"id":"employee","name":"员工","prefixUrl":"/acas/user"}]);
|
||||
//顶部导航栏背景颜色
|
||||
const navrbarBackgroundColor = ref<String>('#071635');
|
||||
//默认菜单名称颜色
|
||||
const menuFontTextColor=ref<String>('#ffffff');
|
||||
//活动菜单名称颜色
|
||||
const activeMenuFontTextColor=ref<String>('#4D7FE0');
|
||||
//是否显示顶部导航栏左侧logo
|
||||
const showLogo = ref<boolean>(true);
|
||||
//左侧logo地址
|
||||
const logoAddress= ref<String>('');
|
||||
//头像地址
|
||||
const headAddress= ref<String>(getAssetsImages('head.jpg'));
|
||||
//是否显示顶部导航栏左侧子系统选择功能
|
||||
const showSubSystemTags = ref<boolean>(true);
|
||||
//是否显示个人中心
|
||||
const showPersonalCenter = ref<boolean>(true);
|
||||
//是否显示下载中心
|
||||
const showDownloadCenter = ref<boolean>(true);
|
||||
//是否显示顶部导航栏右侧消息通知
|
||||
const showMessageNotify = ref<boolean>(false);
|
||||
//是否显示多标签导航
|
||||
const tagsView = ref<boolean>(true);
|
||||
//是否固定头部
|
||||
const fixedHeader = ref<boolean>(true);
|
||||
//是否固定tags
|
||||
const fixedTags = ref<boolean>(true);
|
||||
//最大标签数量,默认8,配置0代表不限制
|
||||
const maxTags = ref<Number>(8);
|
||||
//外部模块发送Token
|
||||
const extModuleSendToken = ref<boolean>(false);
|
||||
//网站相关信息配置
|
||||
const websiteInfo=ref({
|
||||
//是否显示
|
||||
show:false,
|
||||
//排列规则,不配置:左;center:居中;end:靠右
|
||||
//justify:"center",
|
||||
//党政机关事业单位网站标识
|
||||
//conacUrl:{"url":"","size":"30"},
|
||||
//其他信息,版本信息、导航与链接等等
|
||||
//otherInfo:[{"name":"xxx公司","url":""},{"name":"示例名称","url":"完整链接地址,不配置,则不跳转"},],
|
||||
})
|
||||
|
||||
const realSetting={tokenKey,loginResultKey,currentLayout, backendServices, logoutRemoveToken, loginAddress, skipLoginAddressTime, loginType, navrbarBackgroundColor, menuFontTextColor, activeMenuFontTextColor, showLogo, logoAddress, headAddress, showSubSystemTags,showPersonalCenter,showDownloadCenter, showMessageNotify,tagsView,fixedHeader,fixedTags,maxTags,extModuleSendToken,currentLayoutColor,websiteInfo}
|
||||
getDefaultConfig(realSetting)
|
||||
getCustConfig(realSetting)
|
||||
return realSetting
|
||||
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { RouteLocationNormalized } from "vue-router";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import { removeSlashes } from "@/framework/utils/standard/dataHandleUtil";
|
||||
|
||||
export interface TagView extends Partial<RouteLocationNormalized> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// setup
|
||||
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
// state
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
const externalViews=reactive(new Set());
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// actions
|
||||
function addVisitedView(view: TagView) {
|
||||
if (visitedViews.value.some((v) => v.path === view.path)) return;
|
||||
|
||||
if(settingsStore.maxTags===visitedViews.value.length){
|
||||
visitedViews.value.shift()
|
||||
}
|
||||
if (view.meta && view.meta.affix) {
|
||||
visitedViews.value.unshift(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findExternalViewsByPrefix(prefix: String) {
|
||||
for (const view of externalViews) {
|
||||
if (view.startsWith(prefix)) {
|
||||
return view
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function delExternalViews(view: TagView) {
|
||||
let url=view.meta.externalUrl
|
||||
if(view.meta.externalOldERP){
|
||||
url = "/"+removeSlashes(view.meta.externalOldERPIndex)+'/vindex.html?mUrl='
|
||||
+"/"+removeSlashes(view.meta.externalOldERPIndex)+"/"+removeSlashes(view.meta.externalUrl) as string;
|
||||
let oldErpUrl=findExternalViewsByPrefix(url);
|
||||
if(oldErpUrl){
|
||||
url=oldErpUrl;
|
||||
}
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
externalViews.delete(url)
|
||||
resolve([...externalViews]);
|
||||
});
|
||||
}
|
||||
|
||||
function addCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
if (cachedViews.value.includes(viewName)) return;
|
||||
if (view.meta?.keepAlive) {
|
||||
cachedViews.value.push(viewName);
|
||||
}
|
||||
}
|
||||
|
||||
function delVisitedView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
for (const [i, v] of visitedViews.value.entries()) {
|
||||
if (v.path === view.path) {
|
||||
visitedViews.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
function delCachedViewByName(name: String) {
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(name);
|
||||
index > -1 && cachedViews.value.splice(index, 1);
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
function delCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
index > -1 && cachedViews.value.splice(index, 1);
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherVisitedViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
visitedViews.value = visitedViews.value.filter((v) => {
|
||||
return v.meta?.affix || v.path === view.path;
|
||||
});
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherCachedViews(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
if (index > -1) {
|
||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
cachedViews.value = [];
|
||||
}
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisitedView(view: TagView) {
|
||||
for (let v of visitedViews.value) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addView(view: TagView) {
|
||||
addVisitedView(view);
|
||||
addCachedView(view);
|
||||
}
|
||||
|
||||
function delView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delVisitedView(view);
|
||||
delCachedView(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delOtherVisitedViews(view);
|
||||
delOtherCachedViews(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delLeftViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index >= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
function delRightViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index <= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
cachedViews.value = [];
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllVisitedViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delAllCachedViews() {
|
||||
return new Promise((resolve) => {
|
||||
cachedViews.value = [];
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
cachedViews,
|
||||
externalViews,
|
||||
addVisitedView,
|
||||
addCachedView,
|
||||
delVisitedView,
|
||||
delCachedView,
|
||||
delCachedViewByName,
|
||||
delOtherVisitedViews,
|
||||
delOtherCachedViews,
|
||||
updateVisitedView,
|
||||
addView,
|
||||
delView,
|
||||
delOtherViews,
|
||||
delLeftViews,
|
||||
delRightViews,
|
||||
delAllViews,
|
||||
delAllVisitedViews,
|
||||
delAllCachedViews,
|
||||
delExternalViews,
|
||||
findExternalViewsByPrefix,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { defineStore } from "pinia";
|
||||
import pinia from "@/framework/store";
|
||||
import Cookies from 'js-cookie'
|
||||
import { ref } from "vue";
|
||||
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||
import {currentRouter} from "@/framework/router";
|
||||
import { getUsermessage } from '@/framework/api/acas/index'
|
||||
|
||||
const settingsStore = useSettingsStore(pinia);
|
||||
const USER_KEY='abacus_user';
|
||||
const TOKEN_KEY=settingsStore.tokenKey
|
||||
const BUSINESS_DOMAIN_KEY='abacus_business_domain'
|
||||
|
||||
/**开发环境初始化 */
|
||||
function devDataInit(){
|
||||
if (import.meta.env.MODE === 'development'){
|
||||
const expires = new Date(new Date() * 1 + 86400000);
|
||||
Cookies.set(TOKEN_KEY, JSON.stringify({"access_token":"666666","refresh_token":"666666","user_type":"employee","scope":"scope","token_type":"token_type","expires_dt":expires.getTime()}), { expires: expires });
|
||||
Cookies.set(USER_KEY, "0", { expires: expires });
|
||||
}
|
||||
}
|
||||
|
||||
/**设置当前业务域 */
|
||||
export function setCurrentBusinessDomain(businessDomain:Object){
|
||||
sessionStorage.setItem(BUSINESS_DOMAIN_KEY, JSON.stringify(businessDomain))
|
||||
return businessDomain
|
||||
}
|
||||
/**获取当前业务域 */
|
||||
export function getCurrentBusinessDomain(){
|
||||
let obj=JSON.parse(sessionStorage.getItem(BUSINESS_DOMAIN_KEY))
|
||||
if(null===obj||undefined===obj) return {}
|
||||
return obj
|
||||
}
|
||||
/**获取令牌Header信息 */
|
||||
export function getTokenHeader(){
|
||||
const obj = Cookies.get(TOKEN_KEY);
|
||||
if (obj) {
|
||||
return {'Authorization':'Bearer '+JSON.parse(obj).access_token}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/**获取Header公共参数 */
|
||||
function getHeaderCommonParam(){
|
||||
let commonParam={}
|
||||
/**获取菜单、模块名称 */
|
||||
const cr=currentRouter()
|
||||
if(undefined!==cr.value.matched){
|
||||
if(undefined!==cr.value.matched[0]){
|
||||
commonParam.menuName=encodeURI(cr.value.matched[0].meta.title)
|
||||
}
|
||||
if(undefined!==cr.value.matched[1]){
|
||||
commonParam.moduleName=encodeURI(cr.value.matched[1].meta.title)
|
||||
}
|
||||
}
|
||||
/**获取业务域 */
|
||||
const cbd=getCurrentBusinessDomain()
|
||||
if(undefined!==cbd.id){
|
||||
commonParam.businessDomain=cbd.id
|
||||
}
|
||||
if(undefined!==cbd.id){
|
||||
commonParam.businessDomainName=encodeURI(cbd.name)
|
||||
}
|
||||
|
||||
return JSON.stringify(commonParam)
|
||||
}
|
||||
|
||||
/**获取所有需要发送给后端的header信息 */
|
||||
export function getAllHeader(){
|
||||
const header = getTokenHeader();
|
||||
header.commonParam=getHeaderCommonParam();
|
||||
return header
|
||||
}
|
||||
|
||||
/**获取令牌 */
|
||||
export function getToken(){
|
||||
let obj = Cookies.get(TOKEN_KEY);
|
||||
if (obj) {
|
||||
obj=JSON.parse(obj)
|
||||
if('string'===typeof obj){
|
||||
obj=JSON.parse(obj)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return null
|
||||
}
|
||||
/**删除令牌 */
|
||||
export function removeToken(){
|
||||
Cookies.remove(TOKEN_KEY)
|
||||
Cookies.remove(USER_KEY)
|
||||
}
|
||||
/**设置令牌 */
|
||||
export function setToken(token){
|
||||
const exp=token.expires_in*1-10;//提前10秒过期,保证前端存储token先于后台token过期
|
||||
const expires = new Date(new Date() * 1 + exp * 1000);
|
||||
token.expires_dt=expires.getTime()
|
||||
delete token.expires_in
|
||||
Cookies.set(TOKEN_KEY, JSON.stringify(token), { expires: expires })
|
||||
return token
|
||||
}
|
||||
/**用户变化检查 */
|
||||
const userChangeCheck=setInterval(function(){
|
||||
let userId=Cookies.get(USER_KEY);
|
||||
let currentUserId=useUserStoreHook().userId;
|
||||
if(userId&¤tUserId!=userId){
|
||||
location.reload();
|
||||
}
|
||||
},5000);
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
devDataInit()
|
||||
// state
|
||||
const tenantId = ref('');
|
||||
const tenantName = ref('');
|
||||
const userId = ref('');
|
||||
const userName = ref('');
|
||||
const dept = ref('');
|
||||
const deptName = ref('');
|
||||
const avatarUrl=ref('')
|
||||
const token = ref('');
|
||||
const refreshToken = ref('');
|
||||
const tokenExpiredTime = ref(0);
|
||||
|
||||
function handleInfo(alr){
|
||||
let userInfo=null;
|
||||
if(undefined!==alr&&null!==alr&&undefined!==alr.user&&null!==alr.user){
|
||||
tenantId.value = alr.user.tenantId;
|
||||
tenantName.value = alr.user.tenantName;
|
||||
userId.value = alr.user.userId;
|
||||
userName.value = alr.user.userName;
|
||||
dept.value = alr.user.deptId;
|
||||
deptName.value = alr.user.deptName;
|
||||
avatarUrl.value=alr.user.avatarUrl;
|
||||
userInfo={userId:userId.value,userName:userName.value,dept:dept.value,deptName:deptName.value,avatarUrl:avatarUrl}
|
||||
}else{
|
||||
console.error("无登录用户信息")
|
||||
userInfo={userId:'',userName:'',dept:'',deptName:'',avatarUrl:''}
|
||||
}
|
||||
if(!userInfo.avatarUrl.value){
|
||||
userInfo.avatarUrl.value=settingsStore.headAddress
|
||||
}
|
||||
}
|
||||
|
||||
// 获取登录用户信息
|
||||
async function getInfo() {
|
||||
let userInfo=null;
|
||||
await getUsermessage().then((data: any) => {
|
||||
userInfo=handleInfo(data);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(userInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// 注销
|
||||
function logout() {
|
||||
if(settingsStore.logoutRemoveToken){
|
||||
removeToken();
|
||||
}
|
||||
resetToken();
|
||||
window.close();//先尝试关闭标签页
|
||||
//不能关闭标签页,跳转登录页面
|
||||
window.location.href = settingsStore.loginAddress
|
||||
}
|
||||
|
||||
// 重置
|
||||
function resetToken() {
|
||||
token.value = "";
|
||||
tenantId.value = "";
|
||||
tenantName.value = "";
|
||||
userId.value = "";
|
||||
userName.value = "";
|
||||
avatarUrl.value="";
|
||||
dept.value = '';
|
||||
token.value = '';
|
||||
}
|
||||
return {
|
||||
tenantId,
|
||||
tenantName,
|
||||
userId,
|
||||
userName,
|
||||
dept,
|
||||
deptName,
|
||||
avatarUrl,
|
||||
token,
|
||||
refreshToken,
|
||||
tokenExpiredTime,
|
||||
|
||||
getInfo,
|
||||
logout,
|
||||
resetToken,
|
||||
};
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function useUserStoreHook() {
|
||||
return useUserStore(pinia);
|
||||
}
|
||||