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:
|
||||
specifier: ^2.2.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:
|
||||
specifier: ^3.5.12
|
||||
version: 3.5.44(typescript@5.5.4)
|
||||
@@ -51,9 +54,6 @@ importers:
|
||||
'@vitejs/plugin-vue':
|
||||
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))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.6.1(postcss@8.5.31)
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
@@ -768,24 +768,12 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -803,11 +791,6 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -816,9 +799,6 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001819:
|
||||
resolution: {integrity: sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -920,9 +900,6 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
electron-to-chromium@1.5.464:
|
||||
resolution: {integrity: sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==}
|
||||
|
||||
element-plus@2.14.7:
|
||||
resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
|
||||
peerDependencies:
|
||||
@@ -960,10 +937,6 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1099,9 +1072,6 @@ packages:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fraction.js@5.3.4:
|
||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
@@ -1378,10 +1348,6 @@ packages:
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
node-releases@2.0.60:
|
||||
resolution: {integrity: sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1468,6 +1434,11 @@ packages:
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
pinia-plugin-persistedstate@3.2.3:
|
||||
resolution: {integrity: sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==}
|
||||
peerDependencies:
|
||||
pinia: ^2.0.0
|
||||
|
||||
pinia@2.3.1:
|
||||
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
|
||||
peerDependencies:
|
||||
@@ -1487,9 +1458,6 @@ packages:
|
||||
resolution: {integrity: sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.31:
|
||||
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1697,12 +1665,6 @@ packages:
|
||||
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -2387,15 +2349,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.1
|
||||
@@ -2408,8 +2361,6 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.11.32: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
@@ -2427,14 +2378,6 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -2442,8 +2385,6 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001819: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -2535,8 +2476,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
electron-to-chromium@1.5.464: {}
|
||||
|
||||
element-plus@2.14.7(vue@3.5.44(typescript@5.5.4)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.2.1
|
||||
@@ -2603,8 +2542,6 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.21.5
|
||||
'@esbuild/win32-x64': 0.21.5
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
@@ -2777,8 +2714,6 @@ snapshots:
|
||||
hasown: 2.0.4
|
||||
mime-types: 2.1.35
|
||||
|
||||
fraction.js@5.3.4: {}
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
@@ -3044,8 +2979,6 @@ snapshots:
|
||||
node-addon-api@7.1.1:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.60: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-wheel-es@1.2.0: {}
|
||||
@@ -3115,6 +3048,10 @@ snapshots:
|
||||
|
||||
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)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
@@ -3142,8 +3079,6 @@ snapshots:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.20
|
||||
@@ -3370,12 +3305,6 @@ snapshots:
|
||||
acorn: 8.19.0
|
||||
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:
|
||||
dependencies:
|
||||
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 节点,而非仅禁用 */
|
||||
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)
|
||||
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') {
|
||||
next(userStore.token ? { path: '/' } : undefined)
|
||||
if (userStore.token) {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
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
|
||||
icon?: string
|
||||
hidden?: boolean
|
||||
@@ -7,6 +9,8 @@ export interface RouteMetaVO {
|
||||
keepAlive?: boolean
|
||||
/** 只有一个子路由时是否始终展示父级菜单 */
|
||||
alwaysShow?: boolean
|
||||
/** 高亮的菜单路径 */
|
||||
activeMenu?: string
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
|
||||
+29
-23
@@ -1,9 +1,5 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
} from 'axios'
|
||||
import type { AxiosError } from 'axios'
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||
import router from '@/router'
|
||||
import type { ApiResponse } from '@/types/common'
|
||||
import { getToken, removeToken } from '@/utils/storage'
|
||||
@@ -36,7 +32,7 @@ function handleUnauthorized(msg: string): void {
|
||||
})
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
// 请求拦截器:注入 Token
|
||||
service.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
@@ -45,27 +41,20 @@ service.interceptors.request.use((config) => {
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
// 响应拦截器:仅处理登录态与网络层异常
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>): unknown => {
|
||||
// 文件流直接透传,不做业务码判断
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response.data
|
||||
}
|
||||
|
||||
const { code, data, msg } = response.data
|
||||
|
||||
if (code === SUCCESS_CODE) {
|
||||
return data
|
||||
return response
|
||||
}
|
||||
|
||||
const { code, msg } = response.data
|
||||
if (code === TOKEN_INVALID_CODE) {
|
||||
handleUnauthorized(msg)
|
||||
return Promise.reject(new Error(msg))
|
||||
return Promise.reject(new Error(msg || '登录状态已过期'))
|
||||
}
|
||||
|
||||
ElMessage.error(msg || '请求失败,请稍后重试')
|
||||
return Promise.reject(new Error(msg || '请求失败'))
|
||||
return response
|
||||
},
|
||||
(error: AxiosError<ApiResponse>) => {
|
||||
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> {
|
||||
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