chore(frontend): 落地 ADS 治理体系并收录前端底座基线代码

This commit is contained in:
zhoulei
2026-08-19 14:39:02 +08:00
parent 775e075495
commit 1f84456421
177 changed files with 18574 additions and 0 deletions
+24
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

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

+297
View File
@@ -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>&nbsp;</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>&nbsp;</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&&params.tableDataCache) params.tableDataCache=[] //重置表格数据缓存
if(params&&params.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&nbsp;&nbsp;&nbsp;&nbsp;</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">手机号&nbsp;&nbsp;&nbsp;&nbsp;</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&&currentUserId!=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);
}

Some files were not shown because too many files have changed in this diff Show More