init
This commit is contained in:
+312
-1
@@ -1,3 +1,314 @@
|
|||||||
{
|
{
|
||||||
"globals": {}
|
"globals": {
|
||||||
|
"Component": true,
|
||||||
|
"ComponentPublicInstance": true,
|
||||||
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"ExtractDefaultPropTypes": true,
|
||||||
|
"ExtractPropTypes": true,
|
||||||
|
"ExtractPublicPropTypes": true,
|
||||||
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
|
"PropType": true,
|
||||||
|
"Ref": true,
|
||||||
|
"VNode": true,
|
||||||
|
"WritableComputedRef": true,
|
||||||
|
"acceptHMRUpdate": true,
|
||||||
|
"asyncComputed": true,
|
||||||
|
"autoResetRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"computedAsync": true,
|
||||||
|
"computedEager": true,
|
||||||
|
"computedInject": true,
|
||||||
|
"computedWithControl": true,
|
||||||
|
"controlledComputed": true,
|
||||||
|
"controlledRef": true,
|
||||||
|
"createApp": true,
|
||||||
|
"createEventHook": true,
|
||||||
|
"createGlobalState": true,
|
||||||
|
"createInjectionState": true,
|
||||||
|
"createPinia": true,
|
||||||
|
"createReactiveFn": true,
|
||||||
|
"createReusableTemplate": true,
|
||||||
|
"createSharedComposable": true,
|
||||||
|
"createTemplatePromise": true,
|
||||||
|
"createUnrefFn": true,
|
||||||
|
"customRef": true,
|
||||||
|
"debouncedRef": true,
|
||||||
|
"debouncedWatch": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"defineStore": true,
|
||||||
|
"eagerComputed": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"extendRef": true,
|
||||||
|
"getActivePinia": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"h": true,
|
||||||
|
"ignorableWatch": true,
|
||||||
|
"inject": true,
|
||||||
|
"injectLocal": true,
|
||||||
|
"isDefined": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"makeDestructurable": true,
|
||||||
|
"mapActions": true,
|
||||||
|
"mapGetters": true,
|
||||||
|
"mapState": true,
|
||||||
|
"mapStores": true,
|
||||||
|
"mapWritableState": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeRouteLeave": true,
|
||||||
|
"onBeforeRouteUpdate": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onClickOutside": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onKeyStroke": true,
|
||||||
|
"onLongPress": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onStartTyping": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
|
"pausableWatch": true,
|
||||||
|
"provide": true,
|
||||||
|
"provideLocal": true,
|
||||||
|
"reactify": true,
|
||||||
|
"reactifyObject": true,
|
||||||
|
"reactive": true,
|
||||||
|
"reactiveComputed": true,
|
||||||
|
"reactiveOmit": true,
|
||||||
|
"reactivePick": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"refAutoReset": true,
|
||||||
|
"refDebounced": true,
|
||||||
|
"refDefault": true,
|
||||||
|
"refThrottled": true,
|
||||||
|
"refWithControl": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"resolveRef": true,
|
||||||
|
"resolveUnref": true,
|
||||||
|
"setActivePinia": true,
|
||||||
|
"setMapStoreSuffix": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"storeToRefs": true,
|
||||||
|
"syncRef": true,
|
||||||
|
"syncRefs": true,
|
||||||
|
"templateRef": true,
|
||||||
|
"throttledRef": true,
|
||||||
|
"throttledWatch": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toReactive": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"tryOnBeforeMount": true,
|
||||||
|
"tryOnBeforeUnmount": true,
|
||||||
|
"tryOnMounted": true,
|
||||||
|
"tryOnScopeDispose": true,
|
||||||
|
"tryOnUnmounted": true,
|
||||||
|
"unref": true,
|
||||||
|
"unrefElement": true,
|
||||||
|
"until": true,
|
||||||
|
"useActiveElement": true,
|
||||||
|
"useAnimate": true,
|
||||||
|
"useArrayDifference": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayFindLast": true,
|
||||||
|
"useArrayIncludes": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useArrayUnique": true,
|
||||||
|
"useAsyncQueue": true,
|
||||||
|
"useAsyncState": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useBase64": true,
|
||||||
|
"useBattery": true,
|
||||||
|
"useBluetooth": true,
|
||||||
|
"useBreakpoints": true,
|
||||||
|
"useBroadcastChannel": true,
|
||||||
|
"useBrowserLocation": true,
|
||||||
|
"useCached": true,
|
||||||
|
"useClipboard": true,
|
||||||
|
"useClipboardItems": true,
|
||||||
|
"useCloned": true,
|
||||||
|
"useColorMode": true,
|
||||||
|
"useConfirmDialog": true,
|
||||||
|
"useCounter": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVar": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useCurrentElement": true,
|
||||||
|
"useCycleList": true,
|
||||||
|
"useDark": true,
|
||||||
|
"useDateFormat": true,
|
||||||
|
"useDebounce": true,
|
||||||
|
"useDebounceFn": true,
|
||||||
|
"useDebouncedRefHistory": true,
|
||||||
|
"useDeviceMotion": true,
|
||||||
|
"useDeviceOrientation": true,
|
||||||
|
"useDevicePixelRatio": true,
|
||||||
|
"useDevicesList": true,
|
||||||
|
"useDisplayMedia": true,
|
||||||
|
"useDocumentVisibility": true,
|
||||||
|
"useDraggable": true,
|
||||||
|
"useDropZone": true,
|
||||||
|
"useElementBounding": true,
|
||||||
|
"useElementByPoint": true,
|
||||||
|
"useElementHover": true,
|
||||||
|
"useElementSize": true,
|
||||||
|
"useElementVisibility": true,
|
||||||
|
"useEventBus": true,
|
||||||
|
"useEventListener": true,
|
||||||
|
"useEventSource": true,
|
||||||
|
"useEyeDropper": true,
|
||||||
|
"useFavicon": true,
|
||||||
|
"useFetch": true,
|
||||||
|
"useFileDialog": true,
|
||||||
|
"useFileSystemAccess": true,
|
||||||
|
"useFocus": true,
|
||||||
|
"useFocusWithin": true,
|
||||||
|
"useFps": true,
|
||||||
|
"useFullscreen": true,
|
||||||
|
"useGamepad": true,
|
||||||
|
"useGeolocation": true,
|
||||||
|
"useId": true,
|
||||||
|
"useIdle": true,
|
||||||
|
"useImage": true,
|
||||||
|
"useInfiniteScroll": true,
|
||||||
|
"useIntersectionObserver": true,
|
||||||
|
"useInterval": true,
|
||||||
|
"useIntervalFn": true,
|
||||||
|
"useKeyModifier": true,
|
||||||
|
"useLastChanged": true,
|
||||||
|
"useLink": true,
|
||||||
|
"useLocalStorage": true,
|
||||||
|
"useMagicKeys": true,
|
||||||
|
"useManualRefHistory": true,
|
||||||
|
"useMediaControls": true,
|
||||||
|
"useMediaQuery": true,
|
||||||
|
"useMemoize": true,
|
||||||
|
"useMemory": true,
|
||||||
|
"useModel": true,
|
||||||
|
"useMounted": true,
|
||||||
|
"useMouse": true,
|
||||||
|
"useMouseInElement": true,
|
||||||
|
"useMousePressed": true,
|
||||||
|
"useMutationObserver": true,
|
||||||
|
"useNavigatorLanguage": true,
|
||||||
|
"useNetwork": true,
|
||||||
|
"useNow": true,
|
||||||
|
"useObjectUrl": true,
|
||||||
|
"useOffsetPagination": true,
|
||||||
|
"useOnline": true,
|
||||||
|
"usePageLeave": true,
|
||||||
|
"useParallax": true,
|
||||||
|
"useParentElement": true,
|
||||||
|
"usePerformanceObserver": true,
|
||||||
|
"usePermission": true,
|
||||||
|
"usePointer": true,
|
||||||
|
"usePointerLock": true,
|
||||||
|
"usePointerSwipe": true,
|
||||||
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredContrast": true,
|
||||||
|
"usePreferredDark": true,
|
||||||
|
"usePreferredLanguages": true,
|
||||||
|
"usePreferredReducedMotion": true,
|
||||||
|
"usePrevious": true,
|
||||||
|
"useRafFn": true,
|
||||||
|
"useRefHistory": true,
|
||||||
|
"useResizeObserver": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useScreenOrientation": true,
|
||||||
|
"useScreenSafeArea": true,
|
||||||
|
"useScriptTag": true,
|
||||||
|
"useScroll": true,
|
||||||
|
"useScrollLock": true,
|
||||||
|
"useSessionStorage": true,
|
||||||
|
"useShare": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"useSorted": true,
|
||||||
|
"useSpeechRecognition": true,
|
||||||
|
"useSpeechSynthesis": true,
|
||||||
|
"useStepper": true,
|
||||||
|
"useStorage": true,
|
||||||
|
"useStorageAsync": true,
|
||||||
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
|
"useSwipe": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
|
"useTextSelection": true,
|
||||||
|
"useTextareaAutosize": true,
|
||||||
|
"useThrottle": true,
|
||||||
|
"useThrottleFn": true,
|
||||||
|
"useThrottledRefHistory": true,
|
||||||
|
"useTimeAgo": true,
|
||||||
|
"useTimeout": true,
|
||||||
|
"useTimeoutFn": true,
|
||||||
|
"useTimeoutPoll": true,
|
||||||
|
"useTimestamp": true,
|
||||||
|
"useTitle": true,
|
||||||
|
"useToNumber": true,
|
||||||
|
"useToString": true,
|
||||||
|
"useToggle": true,
|
||||||
|
"useTransition": true,
|
||||||
|
"useUrlSearchParams": true,
|
||||||
|
"useUserMedia": true,
|
||||||
|
"useVModel": true,
|
||||||
|
"useVModels": true,
|
||||||
|
"useVibrate": true,
|
||||||
|
"useVirtualList": true,
|
||||||
|
"useWakeLock": true,
|
||||||
|
"useWebNotification": true,
|
||||||
|
"useWebSocket": true,
|
||||||
|
"useWebWorker": true,
|
||||||
|
"useWebWorkerFn": true,
|
||||||
|
"useWindowFocus": true,
|
||||||
|
"useWindowScroll": true,
|
||||||
|
"useWindowSize": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchArray": true,
|
||||||
|
"watchAtMost": true,
|
||||||
|
"watchDebounced": true,
|
||||||
|
"watchDeep": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchIgnorable": true,
|
||||||
|
"watchImmediate": true,
|
||||||
|
"watchOnce": true,
|
||||||
|
"watchPausable": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true,
|
||||||
|
"watchThrottled": true,
|
||||||
|
"watchTriggerable": true,
|
||||||
|
"watchWithFilter": true,
|
||||||
|
"whenever": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-83
@@ -29,6 +29,9 @@ importers:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4))
|
version: 2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4))
|
||||||
|
pinia-plugin-persistedstate:
|
||||||
|
specifier: ^3.2.3
|
||||||
|
version: 3.2.3(pinia@2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4)))
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.12
|
specifier: ^3.5.12
|
||||||
version: 3.5.44(typescript@5.5.4)
|
version: 3.5.44(typescript@5.5.4)
|
||||||
@@ -51,9 +54,6 @@ importers:
|
|||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.2.4(vite@5.4.21(@types/node@22.20.5)(sass@1.106.1))(vue@3.5.44(typescript@5.5.4))
|
version: 5.2.4(vite@5.4.21(@types/node@22.20.5)(sass@1.106.1))(vue@3.5.44(typescript@5.5.4))
|
||||||
autoprefixer:
|
|
||||||
specifier: ^10.4.20
|
|
||||||
version: 10.6.1(postcss@8.5.31)
|
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.1
|
specifier: ^8.57.1
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
@@ -768,24 +768,12 @@ packages:
|
|||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
autoprefixer@10.6.1:
|
|
||||||
resolution: {integrity: sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==}
|
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
postcss: ^8.1.0
|
|
||||||
|
|
||||||
axios@1.20.0:
|
axios@1.20.0:
|
||||||
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
baseline-browser-mapping@2.11.32:
|
|
||||||
resolution: {integrity: sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -803,11 +791,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
browserslist@4.29.4:
|
|
||||||
resolution: {integrity: sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==}
|
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -816,9 +799,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001819:
|
|
||||||
resolution: {integrity: sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==}
|
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -920,9 +900,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
electron-to-chromium@1.5.464:
|
|
||||||
resolution: {integrity: sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==}
|
|
||||||
|
|
||||||
element-plus@2.14.7:
|
element-plus@2.14.7:
|
||||||
resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
|
resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -960,10 +937,6 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
escalade@3.2.0:
|
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
escape-string-regexp@4.0.0:
|
escape-string-regexp@4.0.0:
|
||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1099,9 +1072,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
fraction.js@5.3.4:
|
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
|
||||||
|
|
||||||
fs.realpath@1.0.0:
|
fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
@@ -1378,10 +1348,6 @@ packages:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-releases@2.0.60:
|
|
||||||
resolution: {integrity: sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1468,6 +1434,11 @@ packages:
|
|||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@3.2.3:
|
||||||
|
resolution: {integrity: sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==}
|
||||||
|
peerDependencies:
|
||||||
|
pinia: ^2.0.0
|
||||||
|
|
||||||
pinia@2.3.1:
|
pinia@2.3.1:
|
||||||
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
|
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1487,9 +1458,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==}
|
resolution: {integrity: sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
postcss-value-parser@4.2.0:
|
|
||||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
|
||||||
|
|
||||||
postcss@8.5.31:
|
postcss@8.5.31:
|
||||||
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
|
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -1697,12 +1665,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
|
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
update-browserslist-db@1.3.4:
|
|
||||||
resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
browserslist: '>= 4.21.0'
|
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
@@ -2387,15 +2349,6 @@ snapshots:
|
|||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
autoprefixer@10.6.1(postcss@8.5.31):
|
|
||||||
dependencies:
|
|
||||||
browserslist: 4.29.4
|
|
||||||
caniuse-lite: 1.0.30001819
|
|
||||||
fraction.js: 5.3.4
|
|
||||||
picocolors: 1.1.1
|
|
||||||
postcss: 8.5.31
|
|
||||||
postcss-value-parser: 4.2.0
|
|
||||||
|
|
||||||
axios@1.20.0:
|
axios@1.20.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.16.1
|
follow-redirects: 1.16.1
|
||||||
@@ -2408,8 +2361,6 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.11.32: {}
|
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
@@ -2427,14 +2378,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
|
|
||||||
browserslist@4.29.4:
|
|
||||||
dependencies:
|
|
||||||
baseline-browser-mapping: 2.11.32
|
|
||||||
caniuse-lite: 1.0.30001819
|
|
||||||
electron-to-chromium: 1.5.464
|
|
||||||
node-releases: 2.0.60
|
|
||||||
update-browserslist-db: 1.3.4(browserslist@4.29.4)
|
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -2442,8 +2385,6 @@ snapshots:
|
|||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001819: {}
|
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
@@ -2535,8 +2476,6 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
electron-to-chromium@1.5.464: {}
|
|
||||||
|
|
||||||
element-plus@2.14.7(vue@3.5.44(typescript@5.5.4)):
|
element-plus@2.14.7(vue@3.5.44(typescript@5.5.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ctrl/tinycolor': 4.2.1
|
'@ctrl/tinycolor': 4.2.1
|
||||||
@@ -2603,8 +2542,6 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.21.5
|
'@esbuild/win32-ia32': 0.21.5
|
||||||
'@esbuild/win32-x64': 0.21.5
|
'@esbuild/win32-x64': 0.21.5
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
@@ -2777,8 +2714,6 @@ snapshots:
|
|||||||
hasown: 2.0.4
|
hasown: 2.0.4
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
fraction.js@5.3.4: {}
|
|
||||||
|
|
||||||
fs.realpath@1.0.0: {}
|
fs.realpath@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
@@ -3044,8 +2979,6 @@ snapshots:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-releases@2.0.60: {}
|
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-wheel-es@1.2.0: {}
|
normalize-wheel-es@1.2.0: {}
|
||||||
@@ -3115,6 +3048,10 @@ snapshots:
|
|||||||
|
|
||||||
pidtree@0.6.1: {}
|
pidtree@0.6.1: {}
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@3.2.3(pinia@2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4))):
|
||||||
|
dependencies:
|
||||||
|
pinia: 2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4))
|
||||||
|
|
||||||
pinia@2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4)):
|
pinia@2.3.1(typescript@5.5.4)(vue@3.5.44(typescript@5.5.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
@@ -3142,8 +3079,6 @@ snapshots:
|
|||||||
cssesc: 3.0.0
|
cssesc: 3.0.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
postcss-value-parser@4.2.0: {}
|
|
||||||
|
|
||||||
postcss@8.5.31:
|
postcss@8.5.31:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.20
|
nanoid: 3.3.20
|
||||||
@@ -3370,12 +3305,6 @@ snapshots:
|
|||||||
acorn: 8.19.0
|
acorn: 8.19.0
|
||||||
webpack-virtual-modules: 0.6.2
|
webpack-virtual-modules: 0.6.2
|
||||||
|
|
||||||
update-browserslist-db@1.3.4(browserslist@4.29.4):
|
|
||||||
dependencies:
|
|
||||||
browserslist: 4.29.4
|
|
||||||
escalade: 3.2.0
|
|
||||||
picocolors: 1.1.1
|
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
import { useSettingsStore } from '@/store'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
// 刷新后将持久化的主题色重新写入 CSS 变量
|
||||||
|
onMounted(() => settingsStore.setThemeColor(settingsStore.themeColor))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-config-provider :locale="zhCn">
|
||||||
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import type { DashboardSummary } from '@/types/dashboard'
|
||||||
|
|
||||||
|
const BASE_URL = '/dashboard'
|
||||||
|
|
||||||
|
export const DashboardAPI = {
|
||||||
|
/** 首页汇总数据 */
|
||||||
|
summary() {
|
||||||
|
return request<DashboardSummary>({ url: `${BASE_URL}/summary`, method: 'get' })
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -10,7 +10,11 @@ function normalize(binding: DirectiveBinding<AuthValue>): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 校验不通过时直接移除 DOM 节点,而非仅禁用 */
|
/** 校验不通过时直接移除 DOM 节点,而非仅禁用 */
|
||||||
function check(el: HTMLElement, binding: DirectiveBinding<AuthValue>, checker: (code: string) => boolean) {
|
function check(
|
||||||
|
el: HTMLElement,
|
||||||
|
binding: DirectiveBinding<AuthValue>,
|
||||||
|
checker: (code: string) => boolean,
|
||||||
|
) {
|
||||||
const codes = normalize(binding)
|
const codes = normalize(binding)
|
||||||
if (!codes.length) return
|
if (!codes.length) return
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { constantRoutes } from '@/router'
|
||||||
|
import { usePermissionStore } from '@/store'
|
||||||
|
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要缓存的页面:默认全部缓存,meta.keepAlive === false 时排除
|
||||||
|
* 注意:组件 name 必须与路由 name 一致,否则 include 失效
|
||||||
|
*/
|
||||||
|
const cachedViews = computed(() => {
|
||||||
|
const names: string[] = []
|
||||||
|
|
||||||
|
const walk = (routes: RouteRecordRaw[]) => {
|
||||||
|
routes.forEach((route) => {
|
||||||
|
if (route.meta?.hidden) return
|
||||||
|
if (route.name && route.meta?.keepAlive !== false) {
|
||||||
|
names.push(String(route.name))
|
||||||
|
}
|
||||||
|
if (route.children?.length) walk(route.children)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
walk([...constantRoutes, ...permissionStore.routes])
|
||||||
|
return names
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="app-main">
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<transition name="fade-transform" mode="out-in">
|
||||||
|
<keep-alive :include="cachedViews">
|
||||||
|
<component :is="Component" :key="route.path" />
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-main {
|
||||||
|
min-height: calc(100vh - #{$navbar-height});
|
||||||
|
padding: $container-padding;
|
||||||
|
background-color: $bg-page;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ArrowDown, User } from '@element-plus/icons-vue'
|
||||||
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useAppStore, useSettingsStore, useUserStore } from '@/store'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||||
|
|
||||||
|
/** 面包屑:取 matched 中带标题的层级 */
|
||||||
|
const breadcrumbs = computed(() =>
|
||||||
|
route.matched.filter((item) => item.meta?.title).map((item) => item.meta.title as string),
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleThemeChange(color: string | null) {
|
||||||
|
if (color) {
|
||||||
|
settingsStore.setThemeColor(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCommand(command: string) {
|
||||||
|
if (command === 'logout') {
|
||||||
|
await ElMessageBox.confirm('确认退出登录吗?', '提示', { type: 'warning' })
|
||||||
|
await userStore.logout()
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
void router.replace('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar__left">
|
||||||
|
<el-icon class="navbar__icon" @click="appStore.toggleSidebar()">
|
||||||
|
<component :is="appStore.sidebarCollapsed ? 'Expand' : 'Fold'" />
|
||||||
|
</el-icon>
|
||||||
|
<el-breadcrumb separator="/">
|
||||||
|
<el-breadcrumb-item v-for="(title, index) in breadcrumbs" :key="index">
|
||||||
|
{{ title }}
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar__right">
|
||||||
|
<el-color-picker
|
||||||
|
v-model="settingsStore.themeColor"
|
||||||
|
size="small"
|
||||||
|
@change="handleThemeChange"
|
||||||
|
/>
|
||||||
|
<el-icon class="navbar__icon" @click="toggleFullscreen()">
|
||||||
|
<component :is="isFullscreen ? 'Aim' : 'FullScreen'" />
|
||||||
|
</el-icon>
|
||||||
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
|
<span class="navbar__user">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>{{ userStore.nickname || userStore.userInfo?.username }}</span>
|
||||||
|
<el-icon><ArrowDown /></el-icon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $navbar-height;
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid $border-color-light;
|
||||||
|
|
||||||
|
&__left,
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $text-color-regular;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
color: $text-color-regular;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useAppStore, useSettingsStore } from '@/store'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const title = computed(() => import.meta.env.VITE_APP_TITLE || 'Portal')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="settingsStore.showLogo" class="sidebar-logo">
|
||||||
|
<span class="sidebar-logo__mark">P</span>
|
||||||
|
<span v-show="!appStore.sidebarCollapsed" class="sidebar-logo__title">{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sidebar-logo {
|
||||||
|
@include flex-center;
|
||||||
|
|
||||||
|
height: $navbar-height;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 1px solid $border-color-light;
|
||||||
|
|
||||||
|
&__mark {
|
||||||
|
@include flex-center;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background-color: $color-primary;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include text-ellipsis;
|
||||||
|
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps<{ item: RouteRecordRaw }>()
|
||||||
|
|
||||||
|
/** 需要渲染的子菜单(过滤隐藏项) */
|
||||||
|
const visibleChildren = computed(() => (props.item.children ?? []).filter((c) => !c.meta?.hidden))
|
||||||
|
|
||||||
|
/** 只有一个子路由且未声明 alwaysShow 时,直接渲染子路由 */
|
||||||
|
const singleChild = computed(() => {
|
||||||
|
if (visibleChildren.value.length !== 1) return null
|
||||||
|
return props.item.meta?.alwaysShow ? null : visibleChildren.value[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChildren = computed(
|
||||||
|
() =>
|
||||||
|
visibleChildren.value.length > 1 ||
|
||||||
|
(visibleChildren.value.length === 1 && !singleChild.value),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-sub-menu v-if="hasChildren" :index="String(item.path)">
|
||||||
|
<template #title>
|
||||||
|
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
||||||
|
<span>{{ item.meta?.title }}</span>
|
||||||
|
</template>
|
||||||
|
<SidebarItem v-for="child in visibleChildren" :key="child.path" :item="child" />
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<el-menu-item v-else :index="String(singleChild ? singleChild.path : item.path)">
|
||||||
|
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
||||||
|
<template #title>{{ singleChild ? singleChild.meta?.title : item.meta?.title }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter, type RouteRecordRaw } from 'vue-router'
|
||||||
|
import { constantRoutes } from '@/router'
|
||||||
|
import { useAppStore, usePermissionStore } from '@/store'
|
||||||
|
import Logo from './Logo.vue'
|
||||||
|
import SidebarItem from './SidebarItem.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单数据来源:首页骨架的静态子路由 + 后端返回的动态路由
|
||||||
|
*/
|
||||||
|
const menuRoutes = computed<RouteRecordRaw[]>(() => {
|
||||||
|
const root = constantRoutes.find((item) => item.path === '/')
|
||||||
|
const homeChildren = (root?.children ?? []).filter((item) => !item.meta?.hidden)
|
||||||
|
const dynamicRoutes = permissionStore.routes.filter((item) => !item.meta?.hidden)
|
||||||
|
return [...homeChildren, ...dynamicRoutes]
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu as string | undefined) ?? route.path)
|
||||||
|
|
||||||
|
function handleSelect(index: string) {
|
||||||
|
if (index !== route.path) {
|
||||||
|
void router.push(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar">
|
||||||
|
<Logo />
|
||||||
|
<el-scrollbar class="sidebar__scroll">
|
||||||
|
<el-menu
|
||||||
|
class="sidebar__menu"
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="appStore.sidebarCollapsed"
|
||||||
|
:collapse-transition="false"
|
||||||
|
unique-opened
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<SidebarItem v-for="item in menuRoutes" :key="item.path" :item="item" />
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
import AppMain from './components/AppMain.vue'
|
||||||
|
import Navbar from './components/Navbar.vue'
|
||||||
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-wrapper" :class="{ 'app-wrapper--collapsed': appStore.sidebarCollapsed }">
|
||||||
|
<Sidebar class="app-wrapper__sidebar" />
|
||||||
|
<div class="app-wrapper__container">
|
||||||
|
<Navbar />
|
||||||
|
<AppMain />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: $z-index-sidebar;
|
||||||
|
width: $sidebar-width;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: $bg-sidebar;
|
||||||
|
border-right: 1px solid $border-color-light;
|
||||||
|
transition: width $transition-duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
min-height: 100%;
|
||||||
|
margin-left: $sidebar-width;
|
||||||
|
transition: margin-left $transition-duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--collapsed {
|
||||||
|
.app-wrapper__sidebar {
|
||||||
|
width: $sidebar-width-collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-wrapper__container {
|
||||||
|
margin-left: $sidebar-width-collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { setupDirectives } from '@/directives'
|
||||||
|
import router from '@/router'
|
||||||
|
import pinia from '@/store'
|
||||||
|
import '@/styles/index.scss'
|
||||||
|
import '@/styles/element/index.scss'
|
||||||
|
|
||||||
|
// 以下组件为函数式调用,按需导入插件不会自动引入其样式
|
||||||
|
import 'element-plus/theme-chalk/el-message.css'
|
||||||
|
import 'element-plus/theme-chalk/el-message-box.css'
|
||||||
|
import 'element-plus/theme-chalk/el-notification.css'
|
||||||
|
import 'element-plus/theme-chalk/el-loading.css'
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 全局注册图标,支持菜单按名称字符串渲染
|
||||||
|
for (const [name, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(name, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
setupDirectives(app)
|
||||||
|
|
||||||
|
void router.isReady().then(() => {
|
||||||
|
app.mount('#app')
|
||||||
|
})
|
||||||
+5
-1
@@ -52,7 +52,11 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
|
|
||||||
// 已登录访问登录页 → 回首页
|
// 已登录访问登录页 → 回首页
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
next(userStore.token ? { path: '/' } : undefined)
|
if (userStore.token) {
|
||||||
|
next({ path: '/' })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Element Plus 主题覆盖
|
||||||
|
* 仅通过 CSS 变量覆盖,禁止使用 !important 打补丁
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #{$color-primary};
|
||||||
|
--el-font-size-base: 14px;
|
||||||
|
--el-border-radius-base: 4px;
|
||||||
|
--el-table-header-bg-color: #fafafa;
|
||||||
|
--el-table-header-text-color: #{$text-color-primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部进度条 */
|
||||||
|
#nprogress .bar {
|
||||||
|
background: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区表单项间距 */
|
||||||
|
.el-form--inline .el-form-item {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-color-primary;
|
||||||
|
background-color: $bg-page;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color-primary;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面容器:所有业务页面根节点统一使用 */
|
||||||
|
.app-container {
|
||||||
|
padding: $container-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具栏:搜索栏与操作按钮之间 */
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首屏 loading */
|
||||||
|
.app-loading {
|
||||||
|
@include flex-center;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 3px solid $border-color;
|
||||||
|
border-top-color: $color-primary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: app-loading-rotate 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes app-loading-rotate {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(144, 147, 153, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面切换动画 */
|
||||||
|
.fade-transform-enter-active,
|
||||||
|
.fade-transform-leave-active {
|
||||||
|
transition: all $transition-duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(12px);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/** 仪表盘汇总数据 */
|
||||||
|
export interface DashboardSummary {
|
||||||
|
userTotal: number
|
||||||
|
orderTotal: number
|
||||||
|
gmv: number
|
||||||
|
pendingApproval: number
|
||||||
|
trend: TrendPoint[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrendPoint {
|
||||||
|
date: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
+5
-1
@@ -1,5 +1,7 @@
|
|||||||
|
import type { RouteMeta } from 'vue-router'
|
||||||
|
|
||||||
/** 路由元信息(与后端菜单字段保持一致) */
|
/** 路由元信息(与后端菜单字段保持一致) */
|
||||||
export interface RouteMetaVO {
|
export interface RouteMetaVO extends RouteMeta {
|
||||||
title?: string
|
title?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
@@ -7,6 +9,8 @@ export interface RouteMetaVO {
|
|||||||
keepAlive?: boolean
|
keepAlive?: boolean
|
||||||
/** 只有一个子路由时是否始终展示父级菜单 */
|
/** 只有一个子路由时是否始终展示父级菜单 */
|
||||||
alwaysShow?: boolean
|
alwaysShow?: boolean
|
||||||
|
/** 高亮的菜单路径 */
|
||||||
|
activeMenu?: string
|
||||||
roles?: string[]
|
roles?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-23
@@ -1,9 +1,5 @@
|
|||||||
import axios, {
|
import type { AxiosError } from 'axios'
|
||||||
AxiosError,
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||||
type AxiosInstance,
|
|
||||||
type AxiosRequestConfig,
|
|
||||||
type AxiosResponse,
|
|
||||||
} from 'axios'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import type { ApiResponse } from '@/types/common'
|
import type { ApiResponse } from '@/types/common'
|
||||||
import { getToken, removeToken } from '@/utils/storage'
|
import { getToken, removeToken } from '@/utils/storage'
|
||||||
@@ -36,7 +32,7 @@ function handleUnauthorized(msg: string): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器:注入 Token
|
||||||
service.interceptors.request.use((config) => {
|
service.interceptors.request.use((config) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -45,27 +41,20 @@ service.interceptors.request.use((config) => {
|
|||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
// 响应拦截器
|
// 响应拦截器:仅处理登录态与网络层异常
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response: AxiosResponse<ApiResponse>): unknown => {
|
(response: AxiosResponse<ApiResponse>) => {
|
||||||
// 文件流直接透传,不做业务码判断
|
|
||||||
if (response.config.responseType === 'blob') {
|
if (response.config.responseType === 'blob') {
|
||||||
return response.data
|
return response
|
||||||
}
|
|
||||||
|
|
||||||
const { code, data, msg } = response.data
|
|
||||||
|
|
||||||
if (code === SUCCESS_CODE) {
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { code, msg } = response.data
|
||||||
if (code === TOKEN_INVALID_CODE) {
|
if (code === TOKEN_INVALID_CODE) {
|
||||||
handleUnauthorized(msg)
|
handleUnauthorized(msg)
|
||||||
return Promise.reject(new Error(msg))
|
return Promise.reject(new Error(msg || '登录状态已过期'))
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.error(msg || '请求失败,请稍后重试')
|
return response
|
||||||
return Promise.reject(new Error(msg || '请求失败'))
|
|
||||||
},
|
},
|
||||||
(error: AxiosError<ApiResponse>) => {
|
(error: AxiosError<ApiResponse>) => {
|
||||||
const status = error.response?.status
|
const status = error.response?.status
|
||||||
@@ -84,9 +73,26 @@ service.interceptors.response.use(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一请求入口
|
* 统一请求入口:剥离外层响应结构,直接返回业务数据
|
||||||
* @returns 直接返回业务数据(已在响应拦截器中剥离外层结构)
|
* @param config 请求配置,responseType 为 blob 时直接返回文件流
|
||||||
*/
|
*/
|
||||||
export default function request<T>(config: AxiosRequestConfig): Promise<T> {
|
export default function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
return service.request<unknown, T>(config)
|
const { responseType, ...rest } = config
|
||||||
|
const finalConfig: AxiosRequestConfig = responseType ? { ...rest, responseType } : rest
|
||||||
|
|
||||||
|
const pending = service(finalConfig) as unknown as Promise<AxiosResponse<ApiResponse<T>>>
|
||||||
|
|
||||||
|
return pending.then((response) => {
|
||||||
|
if (responseType === 'blob') {
|
||||||
|
return response.data as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, data, msg } = response.data
|
||||||
|
if (code === SUCCESS_CODE) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(msg || '请求失败,请稍后重试')
|
||||||
|
return Promise.reject(new Error(msg || '请求失败'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { DashboardAPI } from '@/api/dashboard'
|
||||||
|
import type { DashboardSummary } from '@/types/dashboard'
|
||||||
|
|
||||||
|
defineOptions({ name: 'Dashboard' })
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const summary = ref<DashboardSummary>()
|
||||||
|
|
||||||
|
const cards = computed(() => [
|
||||||
|
{ label: '用户总数', value: summary.value?.userTotal ?? 0, icon: 'User', color: '#409eff' },
|
||||||
|
{ label: '订单总数', value: summary.value?.orderTotal ?? 0, icon: 'Tickets', color: '#67c23a' },
|
||||||
|
{ label: '交易额', value: summary.value?.gmv ?? 0, icon: 'Money', color: '#e6a23c' },
|
||||||
|
{
|
||||||
|
label: '待审批',
|
||||||
|
value: summary.value?.pendingApproval ?? 0,
|
||||||
|
icon: 'Clock',
|
||||||
|
color: '#f56c6c',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
/** 趋势柱状图最大高度基准 */
|
||||||
|
const maxTrendValue = computed(() =>
|
||||||
|
Math.max(1, ...(summary.value?.trend ?? []).map((item) => item.value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
async function loadSummary() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
summary.value = await DashboardAPI.summary()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadSummary)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="loading" class="app-container dashboard">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col v-for="card in cards" :key="card.label" :xs="24" :sm="12" :lg="6">
|
||||||
|
<el-card class="dashboard__card" shadow="never">
|
||||||
|
<div class="dashboard__card-body">
|
||||||
|
<el-icon :style="{ color: card.color }"><component :is="card.icon" /></el-icon>
|
||||||
|
<div>
|
||||||
|
<p class="dashboard__label">{{ card.label }}</p>
|
||||||
|
<p class="dashboard__value">{{ card.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-card class="dashboard__trend" shadow="never">
|
||||||
|
<template #header>近 7 日趋势</template>
|
||||||
|
<div class="dashboard__chart">
|
||||||
|
<div v-for="point in summary?.trend ?? []" :key="point.date" class="dashboard__bar">
|
||||||
|
<div
|
||||||
|
class="dashboard__bar-inner"
|
||||||
|
:style="{ height: `${(point.value / maxTrendValue) * 100}%` }"
|
||||||
|
/>
|
||||||
|
<span class="dashboard__bar-label">{{ point.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dashboard {
|
||||||
|
&__card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-color-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chart {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
height: 200px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bar {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bar-inner {
|
||||||
|
width: 32px;
|
||||||
|
background-color: $color-primary;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
transition: height 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bar-label {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
defineOptions({ name: 'NotFound' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="not-found">
|
||||||
|
<h1 class="not-found__code">404</h1>
|
||||||
|
<p class="not-found__desc">抱歉,您访问的页面不存在或没有访问权限</p>
|
||||||
|
<el-button type="primary" @click="router.replace('/')">返回首页</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.not-found {
|
||||||
|
@include flex-center;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 72px;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
margin: 0;
|
||||||
|
color: $text-color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Lock, User } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import type { LoginForm } from '@/types/auth'
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
|
|
||||||
|
defineOptions({ name: 'Login' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
const appTitle = import.meta.env.VITE_APP_TITLE
|
||||||
|
|
||||||
|
const formData = reactive<LoginForm>({ username: 'admin', password: '123456' })
|
||||||
|
|
||||||
|
const rules: FormRules<LoginForm> = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = computed(() => (route.query.redirect as string | undefined) ?? '/')
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login(formData)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
void router.replace(redirect.value)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="login">
|
||||||
|
<el-card class="login__panel" shadow="always">
|
||||||
|
<h2 class="login__title">{{ appTitle }}</h2>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="formData.username" placeholder="请输入用户名" :prefix-icon="User" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:prefix-icon="Lock"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" class="login__submit" :loading="loading" @click="handleSubmit">
|
||||||
|
登 录
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
<p class="login__tip">演示账号:admin / 123456(本地 mock 不校验密码)</p>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login {
|
||||||
|
@include flex-center;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #2b5876, #4e4376);
|
||||||
|
|
||||||
|
&__panel {
|
||||||
|
width: 380px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tip {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { UserAPI } from '@/api/system/user'
|
||||||
|
import { USER_STATUS_OPTIONS, UserStatusEnum } from '@/enums/user'
|
||||||
|
import type { UserForm as UserFormData } from '@/types/system/user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'UserForm' })
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'success'): void }>()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const confirmLoading = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const formData = reactive<UserFormData>({
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
status: UserStatusEnum.ENABLED,
|
||||||
|
roleNames: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => (formData.id ? '编辑用户' : '新增用户'))
|
||||||
|
|
||||||
|
const rules: FormRules<UserFormData> = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
|
||||||
|
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
|
||||||
|
email: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }],
|
||||||
|
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
Object.assign(formData, {
|
||||||
|
id: undefined,
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
status: UserStatusEnum.ENABLED,
|
||||||
|
roleNames: [],
|
||||||
|
})
|
||||||
|
void nextTick(() => formRef.value?.clearValidate())
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(id?: number) {
|
||||||
|
visible.value = true
|
||||||
|
resetForm()
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
const detail = await UserAPI.detail(id)
|
||||||
|
Object.assign(formData, {
|
||||||
|
id: detail.id,
|
||||||
|
username: detail.username,
|
||||||
|
nickname: detail.nickname,
|
||||||
|
mobile: detail.mobile,
|
||||||
|
email: detail.email,
|
||||||
|
status: detail.status,
|
||||||
|
roleNames: detail.roleNames ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
confirmLoading.value = true
|
||||||
|
try {
|
||||||
|
if (formData.id) {
|
||||||
|
await UserAPI.update(formData.id, formData)
|
||||||
|
} else {
|
||||||
|
await UserAPI.create(formData)
|
||||||
|
}
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
visible.value = false
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
confirmLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" :title="title" width="520px" :close-on-click-modal="false">
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="formData.username" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="formData.nickname" placeholder="请输入昵称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="mobile">
|
||||||
|
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="formData.status">
|
||||||
|
<el-radio v-for="option in USER_STATUS_OPTIONS" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="confirmLoading" @click="handleSubmit">确 定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Delete, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { UserAPI } from '@/api/system/user'
|
||||||
|
import { USER_STATUS_OPTIONS, UserStatusEnum } from '@/enums/user'
|
||||||
|
import type { UserQuery, UserVO } from '@/types/system/user'
|
||||||
|
import UserForm from './components/UserForm.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'User' })
|
||||||
|
|
||||||
|
const formRef = ref<InstanceType<typeof UserForm>>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const total = ref(0)
|
||||||
|
const list = ref<UserVO[]>([])
|
||||||
|
const selectedIds = ref<number[]>([])
|
||||||
|
|
||||||
|
const queryParams = reactive<UserQuery>({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
username: '',
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedIdStr = computed(() => selectedIds.value.join(','))
|
||||||
|
|
||||||
|
/** 分页参数变化后需要重新拉取数据 */
|
||||||
|
watch([() => queryParams.pageNum, () => queryParams.pageSize], handleQuery)
|
||||||
|
|
||||||
|
async function handleQuery() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { list: rows, total: count } = await UserAPI.query(queryParams)
|
||||||
|
list.value = rows
|
||||||
|
total.value = count
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
Object.assign(queryParams, { pageNum: 1, pageSize: 10, username: '', status: undefined })
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(selection: UserVO[]) {
|
||||||
|
selectedIds.value = selection.map((item) => item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openForm(row?: Partial<UserVO>) {
|
||||||
|
formRef.value?.open(row?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row?: Partial<UserVO>) {
|
||||||
|
const ids = row ? String(row.id) : selectedIdStr.value
|
||||||
|
if (!ids) {
|
||||||
|
ElMessage.warning('请先选择要删除的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await ElMessageBox.confirm('确认删除选中的数据吗?删除后不可恢复', '提示', { type: 'warning' })
|
||||||
|
await UserAPI.delete(ids)
|
||||||
|
|
||||||
|
// 删除末页最后一条时回退页码,避免出现空列表
|
||||||
|
const remain = total.value - ids.split(',').length
|
||||||
|
const maxPage = Math.max(1, Math.ceil(remain / queryParams.pageSize))
|
||||||
|
queryParams.pageNum = Math.min(queryParams.pageNum, maxPage)
|
||||||
|
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(handleQuery)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-form :model="queryParams" inline @submit.prevent="handleQuery">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
|
||||||
|
<el-option
|
||||||
|
v-for="option in USER_STATUS_OPTIONS"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="Search" @click="handleQuery">查询</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div>
|
||||||
|
<el-button v-hasPerm="'sys:user:add'" type="primary" :icon="Plus" @click="openForm()">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-hasPerm="'sys:user:delete'"
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
:disabled="!selectedIds.length"
|
||||||
|
@click="handleDelete()"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table v-loading="loading" :data="list" border @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||||||
|
<el-table-column prop="deptName" label="部门" min-width="120" />
|
||||||
|
<el-table-column prop="mobile" label="手机号" min-width="130" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === UserStatusEnum.ENABLED ? 'success' : 'info'">
|
||||||
|
{{ row.status === UserStatusEnum.ENABLED ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||||
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-hasPerm="'sys:user:edit'" link type="primary" @click="openForm(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPerm="'sys:user:delete'" link type="danger" @click="handleDelete(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="暂无数据" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="queryParams.pageNum"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
class="pagination"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UserForm ref="formRef" @success="handleQuery" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pagination {
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user