From be81587ef4c7a1711884d7ca8f27cbf85e2d7558 Mon Sep 17 00:00:00 2001 From: whychdw <496960745@qq.com> Date: 星期六, 26 四月 2025 16:02:08 +0800 Subject: [PATCH] 内容提交 --- src/directive/index.ts | 9 src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue | 50 .prettierrc.yaml | 41 src/assets/icons/bilibili.svg | 1 src/assets/icons/user.svg | 1 src/layout/index.vue | 304 + src/views/demo/multi-level/children/children/level3-2.vue | 5 src/assets/icons/github.svg | 1 src/components/Upload/MultiImageUpload.vue | 215 src/assets/icons/code.svg | 1 src/api/system/dept.api.ts | 130 .vscode/extensions.json | 11 src/components/Upload/SingleImageUpload.vue | 202 src/views/system/user/components/UserImport.vue | 199 src/views/dashboard/index.vue | 227 src/styles/variables.scss | 58 src/assets/images/user-logo.gif | 0 src/types/request-result.ts | 58 .husky/commit-msg | 1 src/assets/icons/close.svg | 1 src/components/Dict/index.vue | 133 src/settings.ts | 35 src/views/dashboard/compoents/downloadFile.vue | 133 src/assets/icons/gitee.svg | 1 mock/auth.mock.ts | 43 src/assets/icons/download.svg | 1 src/assets/icons/up.svg | 1 src/store/modules/user.store.ts | 93 src/styles/dark/css-vars.css | 7 src/views/error/404.vue | 243 uno.config.ts | 77 src/components/SizeSelect/index.vue | 40 src/components/Breadcrumb/index.vue | 86 src/types/shims-vue.d.ts | 5 src/styles/reset.scss | 76 .env.development | 15 src/enums/system/menu.enum.ts | 35 src/api/system/dict.api.ts | 302 + src/directive/permission/index.ts | 64 src/layout/components/Sidebar/components/SidebarMenuItem.vue | 194 .vscode/vue3.0.code-snippets | 23 src/enums/settings/locale.enum.ts | 14 src/components/Hamburger/index.vue | 60 src/utils/downloadFile.ts | 27 src/enums/settings/layout.enum.ts | 53 src/assets/icons/cascader.svg | 1 src/assets/icons/tree.svg | 1 src/components/Dict/DictLabel.vue | 60 src/assets/images/404_cloud.png | 0 src/assets/images/login-bg.jpg | 0 src/views/system/role/index.vue | 451 + src/router/admin.routes.ts | 33 src/styles/variables.module.scss | 11 src/views/login/index.vue | 311 + src/views/profile/index.vue | 217 src/assets/images/login-bg-dark.jpg | 0 src/plugins/icons.ts | 9 src/api/auth.api.ts | 82 src/assets/icons/role.svg | 1 src/assets/icons/gitcode.svg | 1 src/assets/icons/qq.svg | 1 src/views/system/menu/index.vue | 530 + vite.config.ts | 201 src/assets/icons/dict.svg | 1 mock/menu.mock.ts | 1285 ++++ src/assets/icons/file.svg | 1 src/assets/images/401.gif | 0 src/views/error/401.vue | 103 src/api/file.api.ts | 81 src/assets/icons/close_right.svg | 1 .husky/pre-commit | 1 src/enums/common/result.enum.ts | 23 src/layout/components/Sidebar/index.vue | 47 src/styles/index.scss | 28 commitlint.config.cjs | 95 src/store/modules/tags-view.store.ts | 254 src/api/system/role.api.ts | 138 mock/dept.mock.ts | 153 src/utils/getLabelByValue.ts | 18 src/views/system/dept/index.vue | 315 + .stylelintrc.cjs | 42 mock/base.ts | 10 .eslintrc-auto-import.json | 316 + src/assets/icons/fullscreen-exit.svg | 1 src/layout/components/TagsView/index.vue | 433 + src/main.ts | 21 src/utils/index.ts | 41 src/plugins/index.ts | 22 src/utils/theme.ts | 52 src/utils/const_num.js | 40 src/views/system/user/components/DeptTree.vue | 70 src/store/modules/permission.store.ts | 113 src/layout/components/Settings/components/LayoutSelect.vue | 142 src/enums/settings/device.enum.ts | 14 src/assets/icons/backtop.svg | 1 src/assets/icons/csdn.svg | 6 src/store/modules/app.store.ts | 107 src/assets/icons/refresh.svg | 1 src/assets/icons/api.svg | 1 src/store/modules/settings.store.ts | 75 .prettierignore | 12 .stylelintignore | 11 src/assets/images/404.png | 0 index.html | 58 src/components/Upload/FileUpload.vue | 251 src/assets/icons/close_all.svg | 1 public/favicon.ico | 0 src/router/index.ts | 78 .editorconfig | 15 src/components/WangEditor/index.vue | 87 LICENSE | 21 src/assets/icons/client.svg | 1 src/assets/icons/wechat.svg | 1 src/types/router.d.ts | 54 src/utils/request.ts | 38 src/views/demo/internal-doc.vue | 25 src/types/env.d.ts | 33 src/utils/RSA.js | 30 src/views/demo/multi-level/children/level2.vue | 7 src/assets/icons/system.svg | 1 mock/role.mock.ts | 335 + src/layout/components/Sidebar/components/SidebarMixTopMenu.vue | 90 src/views/system/user/components/editUser.vue | 174 CHANGELOG.md | 386 + licenses/vue-element-admin/LICENSE | 21 src/api/system/menu.api.ts | 207 src/assets/icons/down.svg | 1 src/components/Pagination/index.vue | 92 src/views/demo/api/apifox.vue | 27 README.md | 199 src/layout/components/NavBar/index.vue | 40 src/assets/icons/fullscreen.svg | 1 src/layout/components/AppMain/index.vue | 36 tsconfig.json | 33 src/utils/auth.ts | 16 .env.production | 9 src/assets/icons/close_left.svg | 1 mock/user.mock.ts | 245 src/layout/components/Settings/index.vue | 162 src/components/Fullscreen/index.vue | 11 src/views/system/dict/index.vue | 284 + src/assets/icons/captcha.svg | 1 src/components/AppLink/index.vue | 38 .gitignore | 17 src/types/components.d.ts | 97 mock/dict.mock.ts | 307 + src/assets/icons/close_other.svg | 1 src/assets/const/const_user.ts | 45 src/types/global.d.ts | 107 src/assets/icons/cnblogs.svg | 1 src/types/auto-imports.d.ts | 989 +++ src/plugins/permission.ts | 88 src/views/system/user/components/addUser.vue | 173 src/router/customer.routes.ts | 5 src/store/modules/dict.store.ts | 55 src/assets/icons/homepage.svg | 1 src/views/redirect/index.vue | 15 src/layout/components/Sidebar/components/SidebarMenu.vue | 110 src/utils/nprogress.ts | 18 src/enums/settings/theme.enum.ts | 18 eslint.config.js | 99 src/api/system/user.api.ts | 378 + src/views/demo/multi-level/level1.vue | 16 .vscode/vue3.3.code-snippets | 21 src/assets/icons/menu.svg | 1 src/assets/icons/juejin.svg | 1 .vscode/vue3.2.code-snippets | 17 src/assets/logo.png | 0 src/assets/icons/size.svg | 1 licenses/vue3-element-admin/LICENSE | 21 .vscode/settings.json | 81 src/api/software.ts | 83 src/assets/icons/collapse.svg | 1 src/layout/components/NavBar/components/NavbarRight.vue | 132 src/layout/components/Sidebar/components/SidebarLogo.vue | 53 src/api/user.ts | 127 src/assets/icons/document.svg | 1 src/assets/icons/setting.svg | 1 src/store/index.ts | 17 src/utils/formatPassword.js | 9 src/components/IconSelect/index.vue | 207 src/views/system/user/index.vue | 302 + package.json | 122 src/views/dashboard/compoents/uploadSoftware.vue | 161 src/enums/index.ts | 8 src/App.vue | 35 src/views/demo/multi-level/children/children/level3-1.vue | 5 src/views/system/dict/dict-item.vue | 285 + 188 files changed, 16,603 insertions(+), 0 deletions(-) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..00ee2de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +# 琛ㄧず鎵�鏈夋枃浠堕�傜敤 +[*] +charset = utf-8 # 璁剧疆鏂囦欢瀛楃闆嗕负 utf-8 +end_of_line = lf # 鎺у埗鎹㈣绫诲瀷(lf | cr | crlf) +indent_style = space # 缂╄繘椋庢牸锛坱ab | space锛� +indent_size = 2 # 缂╄繘澶у皬 +insert_final_newline = true # 濮嬬粓鍦ㄦ枃浠舵湯灏炬彃鍏ヤ竴涓柊琛� + +# 琛ㄧず浠� md 鏂囦欢閫傜敤浠ヤ笅瑙勫垯 +[*.md] +max_line_length = off # 鍏抽棴鏈�澶ц闀垮害闄愬埗 +trim_trailing_whitespace = false # 鍏抽棴鏈熬绌烘牸淇壀 diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..9f9cda2 --- /dev/null +++ b/.env.development @@ -0,0 +1,15 @@ +#寮�鍙戞ā寮� +VITE_NODE_ENV=development + +# 搴旂敤绔彛 +VITE_APP_PORT=5174 + +# 浠g悊鍓嶇紑 +VITE_APP_BASE_API=/bg/ + +# 鎺ュ彛鍦板潃 +# VITE_APP_API_URL=https://api.youlai.tech # 绾夸笂 +VITE_APP_API_URL=http://localhost:8080/bg/ # 鏈湴 + +# 鍚敤 Mock 鏈嶅姟 +VITE_MOCK_DEV_SERVER=false diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..0caa79b --- /dev/null +++ b/.env.production @@ -0,0 +1,9 @@ +#寮�鍙戞ā寮� +VITE_NODE_ENV=production + +# 鎺ュ彛浠g悊鍓嶇紑 +VITE_APP_BASE_API = '/prod-api' + +# 閮ㄧ讲鐨勫熀纭�璺緞 +#VITE_APP_BASE_PATH = '/template/' # 浠庡煙鍚嶅瓙璺緞/template/ 璁块棶 +VITE_APP_BASE_PATH = './' # 浠庡煙鍚嶆牴鐩綍璁块棶 diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..0e8546f --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,316 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ElNotification": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": 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, + "createReactiveFn": true, + "createReusableTemplate": true, + "createSharedComposable": true, + "createTemplatePromise": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "makeDestructurable": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": 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, + "pausableWatch": true, + "provide": 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, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": 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, + "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, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": 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, + "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, + "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, + "useRoute": true, + "useRouter": true, + "storeToRefs": true, + "whenever": true, + "DirectiveBinding": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "WritableComputedRef": true, + "acceptHMRUpdate": true, + "createPinia": true, + "defineStore": true, + "getActivePinia": true, + "injectLocal": true, + "mapActions": true, + "mapGetters": true, + "mapState": true, + "mapStores": true, + "mapWritableState": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onWatcherCleanup": true, + "provideLocal": true, + "setActivePinia": true, + "setMapStoreSuffix": true, + "useClipboardItems": true, + "useI18n": true, + "useId": true, + "useLink": true, + "useModel": true, + "useTemplateRef": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a2a67f --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.history + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +stats.html +pnpm-lock.yaml diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..fd2bf70 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no-install commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..b18d041 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run lint:lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..44421a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +dist +node_modules +public +.husky +.vscode +.idea +*.sh +*.md + +src/assets +stats.html +pnpm-lock.yaml diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..d9cf0c7 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,41 @@ +# 鍦ㄥ崟鍙傛暟绠ご鍑芥暟涓缁堟坊鍔犳嫭鍙� +arrowParens: "always" +# JSX 澶氳鍏冪礌鐨勯棴鍚堟爣绛惧彟璧蜂竴琛� +bracketSameLine: false +# 瀵硅薄瀛楅潰閲忎腑鐨勬嫭鍙蜂箣闂存坊鍔犵┖鏍� +bracketSpacing: true +# 鑷姩鏍煎紡鍖栧祵鍏ョ殑浠g爜锛堝 Markdown 鍜� HTML 鍐呯殑浠g爜锛� +embeddedLanguageFormatting: "auto" +# 蹇界暐 HTML 绌虹櫧鏁忔劅搴︼紝灏嗙┖鐧借涓洪潪閲嶈鍐呭 +htmlWhitespaceSensitivity: "ignore" +# 涓嶆彃鍏� @prettier 鐨� pragma 娉ㄩ噴 +insertPragma: false +# 鍦� JSX 涓娇鐢ㄥ弻寮曞彿 +jsxSingleQuote: false +# 姣忚浠g爜鐨勬渶澶ч暱搴﹂檺鍒朵负 100 瀛楃 +printWidth: 100 +# 鍦� Markdown 涓繚鐣欏師鏈夌殑鎹㈣鏍煎紡 +proseWrap: "preserve" +# 浠呭湪蹇呰鏃舵坊鍔犲璞″睘鎬х殑寮曞彿 +quoteProps: "as-needed" +# 涓嶈姹傛枃浠跺紑澶存彃鍏� @prettier 鐨� pragma 娉ㄩ噴 +requirePragma: false +# 鍦ㄨ鍙ユ湯灏炬坊鍔犲垎鍙� +semi: true +# 浣跨敤鍙屽紩鍙疯�屼笉鏄崟寮曞彿 +singleQuote: false +# 缂╄繘浣跨敤 2 涓┖鏍� +tabWidth: 2 +# 鍦ㄥ琛屽厓绱犵殑鏈熬娣诲姞閫楀彿锛圗S5 鏀寔鐨勫璞°�佹暟缁勭瓑锛� +trailingComma: "es5" +# 浣跨敤绌烘牸鑰屼笉鏄埗琛ㄧ缂╄繘 +useTabs: false +# Vue 鏂囦欢涓殑 <script> 鍜� <style> 涓嶅鍔犻澶栫殑缂╄繘 +vueIndentScriptAndStyle: false +# 鏍规嵁绯荤粺鑷姩妫�娴嬫崲琛岀 +endOfLine: "auto" +# 瀵� HTML 鏂囦欢搴旂敤鐗瑰畾鏍煎紡鍖栬鍒� +overrides: + - files: "*.html" + options: + parser: "html" diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..f3e9850 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,11 @@ +dist +node_modules +public +.husky +.vscode +.idea +*.sh +*.md + +src/assets +stats.html diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs new file mode 100644 index 0000000..62bd59c --- /dev/null +++ b/.stylelintrc.cjs @@ -0,0 +1,42 @@ +module.exports = { + extends: [ + "stylelint-config-recommended", + "stylelint-config-recommended-scss", + "stylelint-config-recommended-vue/scss", + "stylelint-config-html/vue", + "stylelint-config-recess-order", + ], + + plugins: [ + "stylelint-prettier", // 缁熶竴浠g爜椋庢牸锛屾牸寮忓啿绐佹椂浠� Prettier 瑙勫垯涓哄噯 + ], + overrides: [ + { + files: ["**/*.{vue,html}"], + customSyntax: "postcss-html", + }, + { + files: ["**/*.{css,scss}"], + customSyntax: "postcss-scss", + }, + ], + rules: { + "prettier/prettier": true, // 寮哄埗鎵ц Prettier 鏍煎紡鍖栬鍒欙紙闇�閰嶅悎 .prettierrc 閰嶇疆鏂囦欢锛� + "no-empty-source": null, // 鍏佽绌虹殑鏍峰紡鏂囦欢 + "declaration-property-value-no-unknown": null, // 鍏佽闈炲父瑙勬暟鍊兼牸寮� ,濡� height: calc(100% - 50) + // 鍏佽浣跨敤鏈煡浼被 + "selector-pseudo-class-no-unknown": [ + true, + { + ignorePseudoClasses: ["global", "export", "deep"], + }, + ], + // 鍏佽浣跨敤鏈煡浼厓绱� + "at-rule-no-unknown": [ + true, + { + ignoreAtRules: ["apply", "use", "forward", "extend"], + }, + ], + }, +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4e0266a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "vue.volar", + "antfu.unocss", + "lokalise.i18n-ally", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint", + "editorconfig.editorconfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e655d76 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,81 @@ +{ + "typescript.tsdk": "./node_modules/typescript/lib", + "npm.packageManager": "pnpm", + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.quickSuggestions": { + "other": true, + "comments": true, + "strings": true + }, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "explicit" + }, + "files.eol": "\n", + "search.exclude": { + "**/node_modules": true, + "**/*.log": true, + "**/*.log*": true, + "**/bower_components": true, + "**/dist": true, + "**/elehukouben": true, + "**/.git": true, + "**/.gitignore": true, + "**/.svn": true, + "**/.DS_Store": true, + "**/.idea": true, + "**/.vscode": false, + "**/yarn.lock": true, + "**/tmp": true, + "out": true, + "dist": true, + "node_modules": true, + "CHANGELOG.md": true, + "examples": true, + "res": true, + "screenshots": true, + "yarn-error.log": true, + "**/.yarn": true + }, + "files.exclude": { + "**/.cache": true, + "**/.editorconfig": true, + "**/.eslintcache": true, + "**/bower_components": true, + "**/.idea": true, + "**/tmp": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/.vscode/**": true, + "**/node_modules/**": true, + "**/tmp/**": true, + "**/bower_components/**": true, + "**/dist/**": true, + "**/yarn.lock": true + }, + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.namespace": false, + "i18n-ally.pathMatcher": "{namespaces}/{locale}.{ext}", + "i18n-ally.enabledParsers": ["ts"], + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "zh-CN", + "i18n-ally.enabledFrameworks": [ + "vue", + "react" + ], + "i18n-ally.localesPaths": [ + "src/lang" + ], + "scss.lint.unknownAtRules": "ignore" +} diff --git a/.vscode/vue3.0.code-snippets b/.vscode/vue3.0.code-snippets new file mode 100644 index 0000000..544dbd3 --- /dev/null +++ b/.vscode/vue3.0.code-snippets @@ -0,0 +1,23 @@ +{ + "Vue3.0蹇�熺敓鎴愭ā鏉�": { + "scope": "vue", + "prefix": "Vue3.0", + "body": [ + "<template>", + " <div>${1:test}</div>", + "</template>", + "", + "<script lang=\"ts\">", + "export default {", + " setup() {", + " return {};", + " },", + "};", + "</script>", + "", + "<style lang=\"scss\" scoped></style>", + "" + ], + "description": "Vue3.0" + } +} diff --git a/.vscode/vue3.2.code-snippets b/.vscode/vue3.2.code-snippets new file mode 100644 index 0000000..a083940 --- /dev/null +++ b/.vscode/vue3.2.code-snippets @@ -0,0 +1,17 @@ +{ + "Vue3.2+蹇�熺敓鎴愭ā鏉�": { + "scope": "vue", + "prefix": "Vue3.2+", + "body": [ + "<script setup lang=\"ts\"></script>", + "", + "<template>", + " <div>${1:test}</div>", + "</template>", + "", + "<style lang=\"scss\" scoped></style>", + "" + ], + "description": "Vue3.2+" + } +} diff --git a/.vscode/vue3.3.code-snippets b/.vscode/vue3.3.code-snippets new file mode 100644 index 0000000..705e04f --- /dev/null +++ b/.vscode/vue3.3.code-snippets @@ -0,0 +1,21 @@ +{ + "Vue3.3+defineOptions蹇�熺敓鎴愭ā鏉�": { + "scope": "vue", + "prefix": "Vue3.3+", + "body": [ + "<script setup lang=\"ts\">", + "defineOptions({", + " name: \"\",", + "});", + "</script>", + "", + "<template>", + " <div>${1:test}</div>", + "</template>", + "", + "<style lang=\"scss\" scoped></style>", + "" + ], + "description": "Vue3.3+defineOptions蹇�熺敓鎴愭ā鏉�" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd2cef3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,386 @@ + +# 2.11.5 (2024/6/18) + +## 鉁� feat + +- 鏀寔鍚庣鏂囦欢瀵煎叆([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869) + + +## 馃悰 fix +- vue-dev-tools 鎻掍欢瀵艰嚧鑿滃崟璺敱鍒囨崲鍗℃锛屾殏鏃跺叧闂� ([28349e](https://github.com/youlaitech/vue3-element-admin/commit/28349efe147afab36531ba148eaac3a448fe6c71)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.4 (2024/6/16) + +## 鉁� feat + +- 鎿嶄綔鏍忓鍔爎ender閰嶇疆鍙傛暟([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869) +- 宸︿晶宸ュ叿鏍忓鍔爐ype閰嶇疆鍙傛暟([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001) + +## 鈾伙笍 refactor +- 鏇存崲鏉冮檺鍒嗛厤寮圭獥绫诲瀷涓� drawer 骞舵坊鍔犵埗瀛愯仈鍔ㄥ紑鍏�([2d9193](https://github.com/youlaitech/vue3-element-admin/commit/2d9193c47fd224f01f82b9c0b2bbeb5e7cb33584)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.3 (2024/6/11) + +## 鉁� feat + +- 鏀寔榛樿宸ュ叿鏍忕殑瀵煎叆([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869) +- 娣诲姞CURD瀵煎叆绀轰緥([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 鈾伙笍 refactor +- 淇敼瀵煎嚭鍏ㄩ噺鏁版嵁閫夐」鏂囨湰([904fec](https://github.com/youlaitech/vue3-element-admin/commit/904fecad65217650482fcdbb10ffb7f3d27eb9ea)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 馃悰 fix +- 鑿滃崟鍒楄〃鏈�傞厤el-icon瀵艰嚧鍥炬爣涓嶆樉绀洪棶棰樹慨澶�([e72b68](https://github.com/youlaitech/vue3-element-admin/commit/e72b68337562b5a7ea24ad55bbe00023e1266b40)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.2 (2024/6/8) + +## 鉁� feat + +- 鏀寔琛ㄦ牸杩滅▼绛涢��([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869) +- 鏀寔鏍囩杈撳叆妗�([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869) +- 琛ㄥ崟椤规敮鎸乼ips閰嶇疆([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869) +- 鍓嶇瀵煎嚭鏀寔鍏ㄩ噺鏁版嵁([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869) +- 鏀寔閫変腑鏁版嵁瀵煎嚭([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869) +- 琛ㄦ牸榛樿宸ュ叿鏍忕殑瀵煎嚭銆佹悳绱㈡寜閽鍔犳潈闄愮偣鎺у埗([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869) +- 椤电title鏀寔鍔ㄦ�佽缃�([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui) + +## 鈾伙笍 refactor +- 榛樿宸ュ叿鏍忔敮鎸佽嚜瀹氫箟([#136](https://github.com/youlaitech/vue3-element-admin/pull/136)) [@cshaptx4869](https://github.com/cshaptx4869) +- 鏈厤缃叏閲忓鍑烘帴鍙f椂閫夐」闅愯棌([eab91ef](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 馃悰 fix +- 淇娉ㄩ攢鐧诲嚭鍚巖edirect璺宠浆璺敱鍙傛暟涓㈠け([5626017](https://github.com/youlaitech/vue3-element-admin/commit/562601736731afd20bb1a5140d856f6515720159)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.1 (2024/6/6) + +## 鉁� feat + +- 澧炲姞pagination銆乺equest銆乸arseData閰嶇疆鍙傛暟([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869) +- 澧炲姞杩斿洖椤堕儴鍔熻兘([#120](https://github.com/youlaitech/vue3-element-admin/pull/120)) [@cshaptx4869](https://github.com/cshaptx4869) +- 鏀寔鍓嶇瀵煎嚭([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 鈾伙笍 refactor +- 閲嶆瀯甯冨眬鏍峰紡(瑙e喅椤甸潰鎶栧姩闂)([#116](https://github.com/youlaitech/vue3-element-admin/pull/116)) [@cshaptx4869](https://github.com/cshaptx4869) +- 淇敼CURD绀轰緥缂栬緫寮圭獥灏哄([#121](https://github.com/youlaitech/vue3-element-admin/pull/121)) [@cshaptx4869](https://github.com/cshaptx4869) +- 缁熶竴娉ㄥ唽vue鎻掍欢([#122](https://github.com/youlaitech/vue3-element-admin/pull/122)) [@cshaptx4869](https://github.com/cshaptx4869) +- 榛樿涓婚璺熼殢绯荤粺([#128](https://github.com/youlaitech/vue3-element-admin/pull/128)) [@cshaptx4869](https://github.com/cshaptx4869) +- 澧炲姞"scss.lint.unknownAtRules": "ignore"浠g爜锛岃В鍐硈tyle涓娇鐢ˊapply鎻愮ずunknow at rules@apply鎻愮ず闂([Gitee#22](https://gitee.com/youlaiorg/vue3-element-admin/pulls/22)) [@zjsy521](https://gitee.com/zjsy521) + +## 馃悰 fix +- 淇宸︿晶甯冨眬绉诲姩绔彍鍗曞脊鍑烘牱寮� ([#117](https://github.com/youlaitech/vue3-element-admin/pull/117)) [@cshaptx4869](https://github.com/cshaptx4869) + +- 淇缂栬緫鍚庢湭娓呯┖id鍐嶆柊澧炶彍鍗曡鐩栫殑闂([0e78eeb](https://github.com/youlaitech/vue3-element-admin/commit/0e78eeb75008fa8e9732b1b4e7d7a1ea345c7a1b)) [@haoxianrui](https://github.com/haoxianrui) +- 淇姘村嵃灞傜骇闂([#123](https://github.com/youlaitech/vue3-element-admin/pull/123)) [@cshaptx4869](https://github.com/cshaptx4869) +- 淇娣峰悎甯冨眬鏍峰紡闂([#124](https://github.com/youlaitech/vue3-element-admin/pull/124)) [@cshaptx4869](https://github.com/cshaptx4869) +- 淇鍏抽棴寮圭獥鏃舵病鏈塩learValidate闂([#125](https://github.com/youlaitech/vue3-element-admin/pull/125)) [@andm31](https://github.com/andm31) + + + +# 2.11.0 (2024/5/27) + +## 鉁� feat +- 鑿滃崟娣诲姞璺敱鍙傛暟璁剧疆锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 澧炲姞鍒楄〃閫夋嫨缁勪欢锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 澧炲姞鍒楄〃閫夋嫨缁勪欢浣跨敤绀轰緥锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 澧炲姞defaultToolbar閰嶇疆鍙傛暟锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 琛ㄥ崟寮圭獥鏀寔drawer妯″紡锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 琛ㄥ崟椤瑰鍔燾omputed鍜寃atchEffect閰嶇疆锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鏀寔switch灞炴�т慨鏀癸紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 琛ㄥ崟椤瑰鍔犳枃鏈被鍨嬫敮鎸侊紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍒楄〃鍒楀鍔爏how閰嶇疆椤癸紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鏀寔鎼滅储琛ㄥ崟鏄鹃殣鎺у埗锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鏀寔input灞炴�т慨鏀癸紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- search閰嶇疆鏂板鍑芥暟鑳藉姏鎷撳睍锛坅uthor by [xiudaozhe](https://github.com/xiudaozhe)锛� +- 琛ㄦ牸鏂板鍒楄缃帶鍒讹紙author by [haoxianrui](https://github.com/haoxianrui)锛� +- 鎼滅储娣诲姞灞曞紑鍜屾敹缂╋紙author by [haoxianrui](https://github.com/haoxianrui)锛� +- watch鍑芥暟澧炲姞閰嶇疆椤瑰弬鏁拌繑鍥烇紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� + +## 鈾伙笍 refactor +- 閲嶆瀯鍥炬爣閫夋嫨缁勪欢锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 閲嶆瀯鍒楄〃閫夋嫨缁勪欢榛樿鏍峰紡 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍔犲己瀵硅瘽妗嗚〃鍗曠粍浠跺拰鍒楄〃閫夋嫨缁勪欢锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- routeMeta澧炲姞alwaysShow瀛楁澹版槑锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍒嗛〉缁勪欢澧炲姞婧㈠嚭婊氬姩鏁堟灉锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇鐧诲綍琛ㄥ崟鐨凴ef绫诲瀷锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鐐瑰嚮琛ㄦ牸鍒锋柊鎸夐挳涓嶉噸缃〉鐮侊紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 绛涢�夊垪瓒呭嚭涓�瀹氶珮搴︽粴鍔紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 浼樺寲鍔犲己initFn鍑芥暟锛岃〃鍗曢」澧炲姞initFn鍑芥暟锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 閲嶆瀯watch銆乧omputed銆亀atchEffect璋冪敤锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇敼鎿嶄綔鎴愬姛鎻愮ず锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- PageSearch 鏀圭敤card浣滀负瀹瑰櫒,鏍峰紡鏀圭敤unocss鍐欐硶锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 浼樺寲棣栭〉 loading 鍔ㄧ敾鏁堟灉author by [haoxianrui](https://github.com/haoxianrui)锛� + + +## 馃悰 fix +- 璺敱鏄惁濮嬬粓鏄剧ず涓嶉檺鍒跺彧鏈夐《绾х洰褰曟墠鏈夌殑閰嶇疆锛屽紑鏀捐嚦鑿滃崟 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- sockjs-client 鎶ラ敊 global is not defined 瀵艰嚧寮�鍙戠幆澧冩棤娉曟墦寮� WebSocket 椤甸潰闂淇 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 鍙戦�佺敤鎴烽噸鍚瘑鐮佸姛鑳斤紝鏈�灏戜负6浣嶅瓧绗︼紙灏忎簬6浣嶇櫥闄嗘椂涓嶅厑璁哥殑闂锛� 锛坅uthor by [dreamnyj](https://gitee.com/dreamnyj)锛� +- 淇绯荤粺璁剧疆闈㈡澘婊氬姩鏉¢棶棰橈紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇琛ㄥ崟鎻掓Ы澶辨晥闂锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇敼tagsview鍒锋柊涓㈠けquery闂锛坅uthor by [xiudaozhe](https://github.com/xiudaozhe)锛� + +## 馃摝锔� build +- 鍗囩骇 NPM 鍖呯増鏈嚦鏈�鏂� 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 鈿欙笍 ci +- 瑙勬暣鑴氭湰鎵ц鍛戒护锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� + + +# 2.10.1 (2024/5/4) + +## 鈾伙笍 refactor +- 鎶界CURD鐨勪娇鐢ㄩ儴鍒嗕唬鐮佷负Hooks瀹炵幇锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇敼CURD瀵煎叆鏉冮檺鐐规爣璇嗗悕锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- cURD琛ㄥ崟瀛楁鏀寔watch鐩戝惉锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- cURD琛ㄥ崟input鏀寔number淇グ锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- cURD琛ㄥ崟缁勪欢鏀寔checkbox澶氶�夋锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 浼樺寲axios鍝嶅簲鏁版嵁TS绫诲瀷鎻愮ず锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇敼CURD琛ㄥ崟缁勪欢鑷畾涔夌被鍨嬬殑attrs浼犲�硷紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍚屾閲嶇疆瀵嗙爜鎸夐挳鏉冮檺鏍囪瘑閲嶅懡鍚嶏紙author by [haoxianrui](https://github.com/haoxianrui)锛� +- 閲嶆瀯API涓洪潤鎬佹柟娉曞疄鐜版ā鍧楀寲绠$悊锛屽苟灏唗ypes.ts閲嶅懡鍚嶄负model.ts鐢ㄤ簬瀛樻斁鎺ュ彛妯″瀷瀹氫箟锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + + +## 馃悰 fix +- sockjs-client 鎶ラ敊 global is not defined 瀵艰嚧寮�鍙戠幆澧冩棤娉曟墦寮� WebSocket 椤甸潰闂淇 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 涓婚棰滆壊璁剧疆瑕嗙洊鏆楅粦妯″紡涓媏l-table琛屾縺娲荤殑鑳屾櫙鑹查棶棰樹慨澶� 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 淇鍥燗PI鎺ュ彛璋冩暣鑰屽奖鍝嶇殑璋冪敤椤甸潰鐨勯棶棰� 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 馃摝锔� build +- 鍗囩骇 NPM 鍖呯増鏈嚦鏈�鏂� 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + + +# 2.10.0 (2024/4/26) +## 鉁� feat +- 灏佽澧炲垹鏀规煡缁勪欢锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 闆嗘垚 vite-plugin-vue-devtools 鎻掍欢锛坅uthor by [Tricker39](https://github.com/Tricker39)锛� +- 澧炲姞CURD閰嶇疆鍖栧疄鐜帮紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� + + +# 2.9.3 (2024/04/14) +## 鉁� feat +- 澧炲姞vue鏂囦欢浠g爜鐗囨锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鑿滃崟 hover 鑳屾櫙鑹叉坊鍔犲�煎叏灞�SCSS鍙橀噺杩涜鎺у埗锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 鈾伙笍 refactor +- 鍔犲己鍩虹鍥介檯鍖栵紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 澧炲姞璇█鍜屽竷灞�澶у皬鏋氫妇绫诲瀷锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 澧炲姞渚ц竟鏍忕姸鎬佹灇涓剧被鍨嬶紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 浣跨敤甯冨眬鏋氫妇鏇挎崲瀛楅潰閲忥紙author by [haoxianrui](https://github.com/haoxianrui)锛� +- 鎺у埗鍙颁娇鐢ㄩ潤鎬佹暟鎹惊鐜覆鏌擄紙author by [april](mailto:april@zen-game.cn)锛� +- 鏈湴缂撳瓨鐨� token 鍙橀噺閲嶅懡鍚嶏紙author by [haoxianrui](https://github.com/haoxianrui)锛� +- 瀹屽杽 Vite 鐜鍙橀噺绫诲瀷澹版槑锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 馃悰 fix +- 淇鏋勫缓鏃舵彁绀篿conComponent.name鍙兘涓簎ndefined鐨勬姤閿� 锛坅uthor by [wangji1042](https://github.com/wangji1042)锛� +- 淇娴忚鍣ㄥ瘑鐮佽嚜鍔ㄥ~鍏呮椂鍙兘瀛樺湪鐨勬姤閿� 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇eslint鎶ラ敊锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 绉诲姩绔笅鐐瑰嚮宸︿晶鑿滃崟鑺傜偣鍚庡叧闂晶杈规爮锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 娣诲姞 size 绫诲瀷鏂█淇绫诲瀷鎶ラ敊锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 馃摝锔� build +- husky9.x鐗堟湰閫傞厤 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍗囩骇 npm 鍖呯増鏈嚦鏈�鏂帮紙author by [haoxianrui](https://github.com/haoxianrui)锛� + +# 2.9.2 (2024/03/05) +## 鉁� feat +- vscode寮�鍙戞墿灞曟帹鑽愶紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 瀹屽杽鍩虹澧炲垹鏀规煡Mock鎺ュ彛锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 鈾伙笍 refactor +- 淇敼login瀵嗙爜妗嗗姛鑳藉疄鐜帮紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 寮卞寲椤甸潰杩涘叆鍔ㄧ敾鏁堟灉锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍙栨秷鎺ㄨ崘TypeScript Vue Plugin 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 缃戠珯鍔犺浇鍔ㄧ敾鏇挎崲 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 浼樺寲涓婚鍜屼富棰樿壊鐩戝惉锛岄伩鍏嶅涓〉闈㈤噸澶嶅垵濮嬪寲 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� + +## 馃悰 fix +- AppMain 楂樺害鍦ㄩ潪鍥哄畾澶撮儴涓嶆纭鑷村嚭鐜版粴鍔ㄦ潯闂淇 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 淇娣峰悎妯″紡寮�鍚浐瀹欻ead鏃剁殑鏍峰紡闂 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 璁剧疆闈㈡澘缁熶竴瀛椾綋澶у皬 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� + +## 馃摝锔廱uild +- 閫氳繃env閰嶇疆鎺у埗mock鏈嶅姟 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 鍗囩骇渚濊禆鍖呰嚦鏈�鏂扮増鏈� 锛坅uthor by [haoxianrui](https://github.com/haoxianrui)锛� +- 瀹氫箟vite鍏ㄥ眬甯搁噺鏇挎崲椤圭洰鏍囬鍜岀増鏈� 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� + +# 2.9.1 (2024/02/28) +## 鈾伙笍 refactor +- 椤圭洰閰嶇疆鎸夐挳绉诲叆navbar锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 浼樺寲user鏁版嵁瀹氫箟锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 缁熶竴璁剧疆鏍忕殑 SVG 鍥炬爣椋庢牸 + +## 馃悰 fix +- 瑙勬暣涓�浜涘紑鍙戜緷璧栵紙author by [cshaptx4869](https://github.com/cshaptx4869)锛� +- 淇鐧诲綍椤典富棰樺垏鎹㈤棶棰� 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� + +## 馃殌 pref + +- 鍘嬬缉鍥剧墖璧勬簮 锛坅uthor by [cshaptx4869](https://github.com/cshaptx4869)锛� + + +# 2.9.0 (2024/02/25) + +## 鉁� feat +- 寮曞叆 animate.css 鍔ㄧ敾搴� +- 鏂板姘村嵃鍜岄厤缃� +- 鍔ㄦ�佽矾鐢辫彍鍗曟敮鎸� element plus 鐨勫浘鏍� + +## 鈾伙笍 refactor +- Layout 甯冨眬閲嶆瀯鍜岀浉鍏抽棶棰樹慨澶� +- sass 浣跨敤 @use 鏇夸唬 @import 寮曞叆澶栭儴鏂囦欢鎸囦护 + +## 馃悰 fix +- 淇绠$悊椤甸潰閮ㄥ垎寮圭獥鏃犳硶鎵撳紑闂 +- 涓婚棰滆壊璁剧疆鎸夐挳 hover 绛夋湭鍙樺寲闂淇 + + +# 2.8.1 (2024/01/10) + +## 鉁� feat +- 鏇挎崲 Mock 瑙e喅鏂规 vite-plugin-mock 涓� vite-plugin-mock-dev-server 閫傞厤 Vite5 + +# 2.8.0 (2023/12/27) + +## 猬嗭笍 chore +- 鍗囩骇 Vite4 鑷� Vite5 + +# 2.7.1 (2023/12/12) + +## 鈾伙笍 refactor +- 灏嗘墦鍖呭悗鐨勬枃浠惰繘琛屽垎绫� 锛坅uthor by [ityangzhiwen](https://gitee.com/ityangzhiwen)锛� + +# 2.7.0 (2023/11/19) + +## 鈾伙笍 refactor +- 浠g爜閲嶆瀯浼樺寲 +- 淇敼鑷姩瀵煎叆缁勪欢绫诲瀷澹版槑鏂囦欢璺緞 +- 瀹屽杽 typescript 绫诲瀷 + +## 馃悰 fix +- 淇绠$悊椤甸潰閮ㄥ垎寮圭獥鏃犳硶鎵撳紑闂 + + +# 2.7.0 (2023/11/19) + +## 鈾伙笍 refactor +- 浠g爜閲嶆瀯 +- 淇敼鑷姩瀵煎叆缁勪欢绫诲瀷澹版槑鏂囦欢璺緞 +- 瀹屽杽 typescript 绫诲瀷 + +## 馃悰 fix +- 淇绠$悊椤甸潰閮ㄥ垎寮圭獥鏃犳硶鎵撳紑闂 + + +# 2.6.3 (2023/10/22) + +## 鉁� feat +- 鑿滃崟绠$悊鏂板鐩綍鍙湁涓�绾у瓙璺敱鏄惁濮嬬粓鏄剧ず(alwaysShow)鍜岃矾鐢遍〉闈㈡槸鍚︾紦瀛�(keepAlive)鐨勯厤缃� +- 鎺ュ彛鏂囨。鏂板 swagger銆乲nife4j +- 寮曞叆鍜屾敮鎸� tsx + +## 鈾伙笍 refactor +- 浠g爜鐦﹁韩锛屾暣鐞嗗苟鍒犻櫎鏈娇鐢ㄧ殑 svg +- 鎺у埗鍙版牱寮忎紭鍖� + +## 馃悰 fix +- 鑿滃崟鏍忔姌鍙犲拰灞曞紑鐨勫浘鏍囨殫榛戞ā寮忔樉绀洪棶棰樹慨澶� + + +# 2.6.2 (2023/10/11) + +## 馃悰 fix +- 涓婚璁剧疆鏈寔涔呭寲闂 +- UnoCSS 鎻掍欢鏃犳櫤鑳芥彁绀� + +## 鈾伙笍 refactor +- WebSocket 婕旂ず鏍峰紡鍜屼唬鐮佷紭鍖� +- 鐢ㄦ埛绠$悊浠g爜閲嶆瀯 + +# 2.6.1 (2023/9/4) + +## 馃悰 fix +- 瀵艰埅椤堕儴妯″紡銆佹贩鍚堟ā寮忔牱寮忓湪鍥哄畾 Header 鍑虹幇鐨勬牱寮忛棶棰樹慨澶� +- 鍥哄畾 Header 娌℃湁鎸佷箙鍖栭棶棰樹慨澶� +- 瀛楀吀鍥炴樉鍏煎 String 鍜� Number 绫诲瀷 + +# 2.6.0 (2023/8/24)馃挜馃挜馃挜 + +## 鉁� feat +- 瀵艰埅椤堕儴妯″紡銆佹贩鍚堟ā寮忔敮鎸侊紙author by [april-tong](https://april-tong.com/)锛� +- 骞冲彴鏂囨。(鍐呭祵)锛坅uthor by [april-tong](https://april-tong.com/)锛� + +# 2.5.0 (2023/8/8) + +## 鉁� feat +- 鏂板 Mock锛坅uthor by [ygcaicn](https://github.com/ygcaicn)锛� +- 鍥炬爣 DEMO锛坅uthor by [ygcaicn](https://github.com/ygcaicn)锛� + +## 馃悰 fix +- 瀛楀吀鏀寔 Number 绫诲瀷 + +# 2.4.1 (2023/7/20) + +## 鉁� feat +- 鏁村悎 vite-plugin-compression 鎻掍欢鎵撳寘浼樺寲(3.66MB 鈫� 1.58MB) 锛坅uthor by [april-tong](https://april-tong.com/)锛� +- 瀛楀吀缁勪欢灏佽锛坅uthor by [haoxr](https://juejin.cn/user/4187394044331261/posts)锛� + +## 馃悰 fix +- 鍒嗛〉缁勪欢hidden鏃犳晥 +- 绛惧悕鏃犳硶淇濆瓨鑷冲悗绔� +- Git 鎻愪氦 stylelint 鏍¢獙閮ㄥ垎鏈哄櫒鎶ラ敊 + +# 2.4.0 (2023/6/17) + +## 鉁� feat +- 鏂板缁勪欢鏍囩杈撳叆妗嗭紙author by [april-tong](https://april-tong.com/)锛� +- 鏂板缁勪欢绛惧悕锛坅uthor by [april-tong](https://april-tong.com/)锛� +- 鏂板缁勪欢琛ㄦ牸锛坅uthor by [april-tong](https://april-tong.com/)锛� +- Echarts 鍥捐〃娣诲姞涓嬭浇鍔熻兘 author by [april-tong](https://april-tong.com/)锛� + +## 鈾伙笍 refactor +- 闄愬埗鍖呯鐞嗗櫒涓� pnpm 鍜� node 鐗堟湰16+ +- 鑷畾涔夌粍浠惰嚜鍔ㄥ鍏ラ厤缃� +- 鎼滅储妗嗘牱寮忓啓娉曚紭鍖� + +## 馃悰 fix +- 鐢ㄦ埛瀵煎叆鐨勯儴闂ㄥ洖鏄炬垚鏁板瓧闂淇 + +## 猬嗭笍 chore +- element-plus 鐗堟湰鍗囩骇 2.3.5 鈫� 2.3.6 + +# 2.3.1 (2023/5/21) + +## 馃攧 refactor +- 缁勪欢绀轰緥鏂囦欢鍚嶇О浼樺寲 + +# 2.2.2 (2023/5/11) + +## 鉁� feat +- 缁勪欢灏佽绀轰緥娣诲姞婧愮爜鍦板潃 +- 瑙掕壊銆佽彍鍗曘�侀儴闂ㄣ�佸瓧娈垫寜閽坊鍔犳潈闄愭帶鍒� + + +# 2.3.0 (2023/5/12) + +## 猬嗭笍 chore +- vue 鐗堟湰鍗囩骇 3.2.45 鈫� 3.3.1 ([CHANGELOG](https://github.com/vuejs/core/blob/main/CHANGELOG.md)) +- vite 鐗堟湰鍗囩骇 4.3.1 鈫� 4.3.5 + +## 鈾伙笍 refactor +- 浣跨敤 vue 3.3 鐗堟湰鏂扮壒鎬� `defineOptions` 鍦� `setup` 瀹氫箟缁勪欢鍚嶇О锛岀Щ闄ら噸澶嶇殑 `script` 鏍囩 + +# 2.2.2 (2023/5/11) + +## 鉁� feat +- 鐢ㄦ埛鏂板鎻愪氦娣诲姞 `vueUse` 鐨� `useDebounceFn` 鍑芥暟瀹炵幇鎸夐挳闃叉姈鑺傛祦 + + +# 2.2.1 (2023/4/25) + +## 馃悰 fix +- 鍥炬爣閫夋嫨鍣ㄧ粍浠朵娇鐢� `onClickOutside` 鏈帓闄や笅鎷夊脊鍑烘鍏冪礌瀵艰嚧鏃犳硶杈撳叆鎼滅储銆� + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9825cba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present 鏈夋潵寮�婧愮粍缁� + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd0199a --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ + +<div align="center"> + <img alt="vue3-element-admin" width="80" height="80" src="./src/assets/logo.png"> + <h1>vue3-element-admin</h1> + + <img src="https://img.shields.io/badge/Vue-3.5.13-brightgreen.svg"/> + <img src="https://img.shields.io/badge/Vite-6.2.1-green.svg"/> + <img src="https://img.shields.io/badge/Element Plus-2.9.6-blue.svg"/> + <img src="https://img.shields.io/badge/license-MIT-green.svg"/> + <a href="https://gitee.com/youlaiorg" target="_blank"> + <img src="https://img.shields.io/badge/Author-鏈夋潵寮�婧愮粍缁�-orange.svg"/> + </a> +</div> + + + + +<div align="center"> + <a target="_blank" href="http://vue3.youlai.tech/template">馃攳 鍦ㄧ嚎棰勮</a> | <a target="_blank" href="https://juejin.cn/post/7228990409909108793">馃摉 闃呰鏂囨。</a> | <a href="./README.en-US.md">馃寪English +</div> + + +## 椤圭洰绠�浠� + +[vue3-element-template](https://gitee.com/youlaiorg/vue3-element-admin) 鏄� [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) 绮剧畝鐨� Vue3 涓悗鍙板紑鍙戞ā鏉匡紝閲囩敤 Vue3 + Vite6 + TypeScript5 + Element-Plus + Pinia 绛夊墠娌挎妧鏈爤鏋勫缓锛岄厤濂楁彁渚� [Java 鍚庣](https://gitee.com/youlaiorg/youlai-boot) 鍜� [Node 鍚庣](https://gitee.com/youlaiorg/youlai-nest)锛屽畬鍏ㄥ厤璐瑰紑婧愩�� + + +## 椤圭洰鐗硅壊 + +- 馃殌 **鏍稿績浼樺娍** + - **鎶�鏈崌绾�**锛氬熀浜庣粡鍏搁」鐩� [vue-element-admin](https://gitee.com/panjiachen/vue-element-admin) 鐨� Vue3 閲嶆瀯鐗堬紝閬垮厤杩囧害灏佽锛屽涔犳洸绾垮钩缂� + - **寮�绠卞嵆鐢�**锛氶璁句唬鐮佽鑼冦�丟it 鎻愪氦瑙勮寖鍜屽伐绋嬪寲閰嶇疆锛屽唴缃父鐢ㄤ笟鍔$粍浠� + - **鍏ㄦ爤鏂规**锛氭敮鎸佹湰鍦� Mock 鍜岀嚎涓� API 鍒囨崲锛岄厤濂� [Java 鍚庣绯荤粺](https://gitee.com/youlaiorg/youlai-boot) 鍜� [鍦ㄧ嚎鎺ュ彛鏂囨。](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) + +- 馃洝锔� 鏉冮檺浣撶郴 + - **瀹屾暣鍔熻兘**锛氱敤鎴�/瑙掕壊/鑿滃崟/瀛楀吀/閮ㄩ棬浜斾綅涓�浣撶殑鏉冮檺绠$悊绯荤粺 + - **绮剧粏鎺у埗**锛氬姩鎬佽矾鐢卞姞杞� + 鎸夐挳绾ф潈闄愭帶鍒讹紝鏀寔鍥介檯鍖栧璇█鏂规 + +- 馃洜锔� 鎸佺画缁存姢 + - **鐗堟湰鏇存柊**锛氭寔缁窡杩涗富娴佹妧鏈洿鏂帮紝瀹氭湡鍗囩骇渚濊禆鍜屽伐鍏烽摼 + - **澶氱閫傞厤**锛氭彁渚涘熀纭�鐗堛�佸紑鍙戠増銆佺簿绠�鐗堝绉嶅舰鎬侊紝婊¤冻涓嶅悓鍦烘櫙闇�姹� + + +## 椤圭洰棰勮 + + + + + + + +## 椤圭洰婧愮爜 + +| 鐗堟湰绫诲瀷 | 鍔熻兘浜偣 | Gitee 婧愮爜 | GitHub 婧愮爜 | GitCode 闀滃儚 | +|----------------|---------------------------------------|----------------------------------------------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------| +| **鍩虹鐗�** | 鉁� 鏉冮檺浣撶郴 + 馃寪 鍥介檯鍖� + 馃洜锔� 浠g爜鐢熸垚 + 馃帴 婕旂ず妗堜緥 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | +| **寮�鍙戠増** | 鉁� 鏉冮檺绯荤粺 + 馃摝 鍩虹鍔熻兘 | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | [vue3-element-template](https://github.com/youlaitech/vue3-element-template) | [vue3-element-template](https://gitcode.com/youlai/vue3-element-template) | +| **绮剧畝鐗�** | 馃攼 鍩虹鐧诲綍 + 馃З 鏈�灏忓姛鑳介泦 | [vue3-element-admin-thin](https://gitee.com/cshaptx4869/vue3-element-admin-thin) | [vue3-element-admin-thin](https://github.com/youlaitech/vue3-element-admin-thin) | - | +| **Java 鍚庣** | 馃彈锔� SpringBoot 鍏ㄦ爤瑙e喅鏂规 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) | +| **Node 鍚庣** | 馃殌 NestJS 鍏ㄦ爤瑙e喅鏂规 | [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | [youlai-nest](https://github.com/youlaitech/youlai-nest) | [youlai-nest](https://gitcode.com/youlai/youlai-nest) | + + +## 鐜鍑嗗 + + +| 鐜 | 鍚嶇О鐗堟湰 | 涓嬭浇鍦板潃 | +| -------------------- | :----------------------------------------------------------- | ------------------------------------------------------------ | +| **寮�鍙戝伐鍏�** | VSCode | [涓嬭浇](https://code.visualstudio.com/Download) | +| **杩愯鐜** | Node 鈮�18 (鍏朵腑 20.6.0 鐗堟湰涓嶅彲鐢�) | [涓嬭浇](http://nodejs.cn/download) | + + + + +## 椤圭洰鍚姩 + +```bash +# 鍏嬮殕浠g爜 +git clone https://gitee.com/youlaiorg/vue3-element-template.git + +# 鍒囨崲鐩綍 +cd vue3-element-template + +# 瀹夎 pnpm +npm install pnpm -g + +# 璁剧疆闀滃儚婧�(鍙拷鐣�) +pnpm config set registry https://registry.npmmirror.com + +# 瀹夎渚濊禆 +pnpm install + +# 鍚姩杩愯 +pnpm run dev +``` + + + +## 椤圭洰閮ㄧ讲 + +```bash +# 椤圭洰鎵撳寘 +pnpm run build + +# 涓婁紶鏂囦欢鑷宠繙绋嬫湇鍔″櫒 +灏嗘湰鍦版墦鍖呯敓鎴愮殑 dist 鐩綍涓嬬殑鎵�鏈夋枃浠舵嫹璐濊嚦鏈嶅姟鍣ㄧ殑 /usr/share/nginx/html 鐩綍銆� + +# nginx.cofig 閰嶇疆 +server { + listen 80; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + # 鍙嶅悜浠g悊閰嶇疆 + location /prod-api/ { + # api.youlai.tech 鏇挎崲鍚庣API鍦板潃锛屾敞鎰忎繚鐣欏悗闈㈢殑鏂滄潬 / + proxy_pass http://api.youlai.tech/; + } +} +``` + +## 鏈湴Mock + +椤圭洰鍚屾椂鏀寔鍦ㄧ嚎鍜屾湰鍦� Mock 鎺ュ彛锛岄粯璁や娇鐢ㄧ嚎涓婃帴鍙o紝濡傞渶鏇挎崲涓� Mock 鎺ュ彛锛屼慨鏀规枃浠� `.env.development` 鐨� `VITE_MOCK_DEV_SERVER` 涓� `true` **鍗冲彲**銆� + +## 鍚庣鎺ュ彛 + +> 濡傛灉鎮ㄥ叿澶嘕ava寮�鍙戝熀纭�锛屾寜鐓т互涓嬫楠ゅ皢鍦ㄧ嚎鎺ュ彛杞负鏈湴鍚庣鎺ュ彛锛屽垱寤轰紒涓氱骇鍓嶅悗绔垎绂诲紑鍙戠幆澧冿紝鍔╂偍璧板悜鍏ㄦ爤涔嬭矾銆� + +1. 鑾峰彇鍩轰簬 `Java` 鍜� `SpringBoot` 寮�鍙戠殑鍚庣 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot.git) 婧愮爜銆� +2. 鏍规嵁鍚庣宸ョ▼鐨勮鏄庢枃妗� [README.md](https://gitee.com/youlaiorg/youlai-boot#%E9%A1%B9%E7%9B%AE%E8%BF%90%E8%A1%8C) 瀹屾垚鏈湴鍚姩銆� +3. 淇敼 `.env.development` 鏂囦欢涓殑 `VITE_APP_API_URL` 鐨勫�硷紝灏嗗叾浠� https://api.youlai.tech 鏇存敼涓� http://localhost:8989 鍗冲彲銆� + + +## 娉ㄦ剰浜嬮」 + +- **鑷姩瀵煎叆鎻掍欢鑷姩鐢熸垚榛樿鍏抽棴** + + 妯℃澘椤圭洰鐨勭粍浠剁被鍨嬪0鏄庡凡鑷姩鐢熸垚銆傚鏋滄坊鍔犲拰浣跨敤鏂扮殑缁勪欢锛岃鎸夌収鍥剧ず鏂规硶寮�鍚嚜鍔ㄧ敓鎴愩�傚湪鑷姩鐢熸垚瀹屾垚鍚庯紝璁板緱灏嗗叾璁剧疆涓� `false`锛岄伩鍏嶉噸澶嶆墽琛屽紩鍙戝啿绐併�� + +  + +- **椤圭洰鍚姩娴忚鍣ㄨ闂┖鐧�** + + 璇峰崌绾ф祻瑙堝櫒灏濊瘯锛屼綆鐗堟湰娴忚鍣ㄥ唴鏍稿彲鑳戒笉鏀寔鏌愪簺鏂扮殑 JavaScript 璇硶锛屾瘮濡傚彲閫夐摼鎿嶄綔绗� `?.`銆� + +- **椤圭洰鍚屾浠撳簱鏇存柊鍗囩骇** + + 椤圭洰鍚屾浠撳簱鏇存柊鍗囩骇涔嬪悗锛屽缓璁� `pnpm install` 瀹夎鏇存柊渚濊禆涔嬪悗鍚姩 銆� + +- **椤圭洰缁勪欢銆佸嚱鏁板拰寮曠敤鐖嗙孩** + + 閲嶅惎 VSCode 灏濊瘯 + +- **鍏朵粬闂** + + 濡傛灉鏈夊叾浠栭棶棰樻垨鑰呭缓璁紝寤鸿 [ISSUE](https://gitee.com/youlaiorg/vue3-element-admin/issues/new) + + + +## 椤圭洰鏂囨。 + +- [鍩轰簬 Vue3 + Vite + TypeScript + Element-Plus 浠�0鍒�1鎼缓鍚庡彴绠$悊绯荤粺](https://blog.csdn.net/u013737132/article/details/130191394) +- [ESLint+Prettier+Stylelint+EditorConfig 绾︽潫鍜岀粺涓�鍓嶇浠g爜瑙勮寖](https://youlai.blog.csdn.net/article/details/145608723) +- [Husky + Lint-staged + Commitlint + Commitizen + cz-git 閰嶇疆 Git 鎻愪氦瑙勮寖](https://youlai.blog.csdn.net/article/details/145615236) + + +## 鎻愪氦瑙勮寖 + +鎵ц `pnpm run commit` 鍞よ捣 git commit 浜や簰锛屾牴鎹彁绀哄畬鎴愪俊鎭殑杈撳叆鍜岄�夋嫨銆� + + + + +## 椤圭洰缁熻 + + + + +Thanks to all the contributors! + +[](https://github.com/youlaitech/vue3-element-admin/graphs/contributors) + +## G-Star + + + + +## 鍔犵兢浜ゆ祦 + +> **鍏虫敞銆屾湁鏉ユ妧鏈�嶅叕浼楀彿锛岀偣鍑昏彍鍗曗�滀氦娴佺兢鈥濊幏鍙栧姞缇や簩缁寸爜銆�** +> +> 濡傛灉浜岀淮鐮佽繃鏈燂紝璇峰姞寰俊(haoxianrui)澶囨敞銆屽墠绔�嶃�併�屽悗绔�嶆垨銆屽叏鏍堛�嶆媺浣犺繘缇ゃ�� +> +> 浜ゆ祦缇や粎闄愭妧鏈氦娴侊紝涓鸿繃婊ゅ箍鍛婅惀閿�鏆傝姝ら棬妲涳紝鎰熻阿鐞嗚В涓庨厤鍚� + +![鏈夋潵鎶�鏈叕浼楀彿浜岀淮鐮乚(https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png) + diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..4ecb995 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,95 @@ +module.exports = { + // 缁ф壙鐨勮鍒� + extends: ["@commitlint/config-conventional"], + // 鑷畾涔夎鍒� + rules: { + // @see https://commitlint.js.org/#/reference-rules + + // 鎻愪氦绫诲瀷鏋氫妇锛実it鎻愪氦type蹇呴』鏄互涓嬬被鍨� + "type-enum": [ + 2, + "always", + [ + "feat", // 鏂板鍔熻兘 + "fix", // 淇缂洪櫡 + "docs", // 鏂囨。鍙樻洿 + "style", // 浠g爜鏍煎紡锛堜笉褰卞搷鍔熻兘锛屼緥濡傜┖鏍笺�佸垎鍙风瓑鏍煎紡淇锛� + "refactor", // 浠g爜閲嶆瀯锛堜笉鍖呮嫭 bug 淇銆佸姛鑳芥柊澧烇級 + "perf", // 鎬ц兘浼樺寲 + "test", // 娣诲姞鐤忔紡娴嬭瘯鎴栧凡鏈夋祴璇曟敼鍔� + "build", // 鏋勫缓娴佺▼銆佸閮ㄤ緷璧栧彉鏇达紙濡傚崌绾� npm 鍖呫�佷慨鏀� webpack 閰嶇疆绛夛級 + "ci", // 淇敼 CI 閰嶇疆銆佽剼鏈� + "revert", // 鍥炴粴 commit + "chore", // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級 + "wip", // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級 + ], + ], + "subject-case": [0], // subject澶у皬鍐欎笉鍋氭牎楠� + }, + + prompt: { + messages: { + type: "閫夋嫨浣犺鎻愪氦鐨勭被鍨� :", + scope: "閫夋嫨涓�涓彁浜よ寖鍥达紙鍙�夛級:", + customScope: "璇疯緭鍏ヨ嚜瀹氫箟鐨勬彁浜よ寖鍥� :", + subject: "濉啓绠�鐭簿鐐肩殑鍙樻洿鎻忚堪 :\n", + body: '濉啓鏇村姞璇︾粏鐨勫彉鏇存弿杩帮紙鍙�夛級銆備娇鐢� "|" 鎹㈣ :\n', + breaking: '鍒椾妇闈炲吋瀹规�ч噸澶х殑鍙樻洿锛堝彲閫夛級銆備娇鐢� "|" 鎹㈣ :\n', + footerPrefixesSelect: "閫夋嫨鍏宠仈issue鍓嶇紑锛堝彲閫夛級:", + customFooterPrefix: "杈撳叆鑷畾涔塱ssue鍓嶇紑 :", + footer: "鍒椾妇鍏宠仈issue (鍙��) 渚嬪: #31, #I3244 :\n", + generatingByAI: "姝e湪閫氳繃 AI 鐢熸垚浣犵殑鎻愪氦绠�鐭弿杩�...", + generatedSelectByAI: "閫夋嫨涓�涓� AI 鐢熸垚鐨勭畝鐭弿杩�:", + confirmCommit: "鏄惁鎻愪氦鎴栦慨鏀筩ommit ?", + }, + // prettier-ignore + types: [ + { value: "feat", name: "鐗规��: 鉁� 鏂板鍔熻兘", emoji: ":sparkles:" }, + { value: "fix", name: "淇: 馃悰 淇缂洪櫡", emoji: ":bug:" }, + { value: "docs", name: "鏂囨。: 馃摑 鏂囨。鍙樻洿(鏇存柊README鏂囦欢锛屾垨鑰呮敞閲�)", emoji: ":memo:" }, + { value: "style", name: "鏍煎紡: 馃寛 浠g爜鏍煎紡锛堢┖鏍笺�佹牸寮忓寲銆佺己澶辩殑鍒嗗彿绛夛級", emoji: ":lipstick:" }, + { value: "refactor", name: "閲嶆瀯: 馃攧 浠g爜閲嶆瀯锛堜笉淇閿欒涔熶笉娣诲姞鐗规�х殑浠g爜鏇存敼锛�", emoji: ":recycle:" }, + { value: "perf", name: "鎬ц兘: 馃殌 鎬ц兘浼樺寲", emoji: ":zap:" }, + { value: "test", name: "娴嬭瘯: 馃И 娣诲姞鐤忔紡娴嬭瘯鎴栧凡鏈夋祴璇曟敼鍔�", emoji: ":white_check_mark:"}, + { value: "build", name: "鏋勫缓: 馃摝锔� 鏋勫缓娴佺▼銆佸閮ㄤ緷璧栧彉鏇达紙濡傚崌绾� npm 鍖呫�佷慨鏀� vite 閰嶇疆绛夛級", emoji: ":package:"}, + { value: "ci", name: "闆嗘垚: 鈿欙笍 淇敼 CI 閰嶇疆銆佽剼鏈�", emoji: ":ferris_wheel:"}, + { value: "revert", name: "鍥為��: 鈫╋笍 鍥炴粴 commit",emoji: ":rewind:"}, + { value: "chore", name: "鍏朵粬: 馃洜锔� 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級", emoji: ":hammer:"}, + { value: "wip", name: "寮�鍙戜腑: 馃毀 寮�鍙戦樁娈典复鏃舵彁浜�", emoji: ":construction:"}, + ], + useEmoji: true, + emojiAlign: "center", + useAI: false, + aiNumber: 1, + themeColorCode: "", + scopes: [], + allowCustomScopes: true, + allowEmptyScopes: true, + customScopesAlign: "bottom", + customScopesAlias: "custom", + emptyScopesAlias: "empty", + upperCaseSubject: false, + markBreakingChangeMode: false, + allowBreakingChanges: ["feat", "fix"], + breaklineNumber: 100, + breaklineChar: "|", + skipQuestions: [], + issuePrefixes: [ + { value: "closed", name: "closed: ISSUES has been processed" }, + ], + customIssuePrefixAlign: "top", + emptyIssuePrefixAlias: "skip", + customIssuePrefixAlias: "custom", + allowCustomIssuePrefix: true, + allowEmptyIssuePrefix: true, + confirmColorize: true, + maxHeaderLength: Infinity, + maxSubjectLength: Infinity, + minSubjectLength: 0, + scopeOverrides: undefined, + defaultBody: "", + defaultIssues: "", + defaultScope: "", + defaultSubject: "", + }, +}; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..28ccc1b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,99 @@ +// https://eslint.nodejs.cn/docs/latest/use/configure/configuration-files + +import globals from "globals"; +import pluginJs from "@eslint/js"; // JavaScript 瑙勫垯 +import pluginVue from "eslint-plugin-vue"; // Vue 瑙勫垯 +import pluginTypeScript from "@typescript-eslint/eslint-plugin"; // TypeScript 瑙勫垯 + +import parserVue from "vue-eslint-parser"; // Vue 瑙f瀽鍣� +import parserTypeScript from "@typescript-eslint/parser"; // TypeScript 瑙f瀽鍣� + +import configPrettier from "eslint-config-prettier"; // 绂佺敤涓� Prettier 鍐茬獊鐨勮鍒� +import pluginPrettier from "eslint-plugin-prettier"; // 杩愯 Prettier 瑙勫垯 + +// 瑙f瀽鑷姩瀵煎叆閰嶇疆 +import fs from "fs"; +const autoImportConfig = JSON.parse(fs.readFileSync(".eslintrc-auto-import.json", "utf-8")); + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + // 鎸囧畾妫�鏌ユ枃浠跺拰蹇界暐鏂囦欢 + { + files: ["**/*.{js,mjs,cjs,ts,vue}"], + ignores: ["**/*.d.ts"], + }, + // 鍏ㄥ眬閰嶇疆 + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...autoImportConfig.globals, + ...{ + PageQuery: "readonly", + PageResult: "readonly", + OptionType: "readonly", + ResponseData: "readonly", + ExcelResult: "readonly", + TagView: "readonly", + AppSettings: "readonly", + __APP_INFO__: "readonly", + }, + }, + }, + plugins: { prettier: pluginPrettier }, + rules: { + ...configPrettier.rules, // 鍏抽棴涓� Prettier 鍐茬獊鐨勮鍒� + ...pluginPrettier.configs.recommended.rules, // 鍚敤 Prettier 瑙勫垯 + "prettier/prettier": "error", // 寮哄埗 Prettier 鏍煎紡鍖� + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", // 蹇界暐鍙傛暟鍚嶄互 _ 寮�澶寸殑鍙傛暟鏈娇鐢ㄨ鍛� + varsIgnorePattern: "^[A-Z0-9_]+$", // 蹇界暐鍙橀噺鍚嶄负澶у啓瀛楁瘝銆佹暟瀛楁垨涓嬪垝绾跨粍鍚堢殑鏈娇鐢ㄨ鍛婏紙鏋氫妇瀹氫箟鏈娇鐢ㄥ満鏅級 + ignoreRestSiblings: true, // 蹇界暐瑙f瀯璧嬪�间腑鍚岀骇鏈娇鐢ㄥ彉閲忕殑璀﹀憡 + }, + ], + }, + }, + // JavaScript 閰嶇疆 + pluginJs.configs.recommended, + + // TypeScript 閰嶇疆 + { + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], // 鎺掗櫎d.ts鏂囦欢 + languageOptions: { + parser: parserTypeScript, + parserOptions: { + sourceType: "module", + }, + }, + plugins: { "@typescript-eslint": pluginTypeScript }, + rules: { + ...pluginTypeScript.configs.strict.rules, // TypeScript 涓ユ牸瑙勫垯 + "@typescript-eslint/no-explicit-any": "off", // 鍏佽浣跨敤 any + "@typescript-eslint/no-empty-function": "off", // 鍏佽绌哄嚱鏁� + "@typescript-eslint/no-empty-object-type": "off", // 鍏佽绌哄璞$被鍨� + }, + }, + + // Vue 閰嶇疆 + { + files: ["**/*.vue"], + languageOptions: { + parser: parserVue, + parserOptions: { + parser: parserTypeScript, + sourceType: "module", + }, + }, + plugins: { vue: pluginVue, "@typescript-eslint": pluginTypeScript }, + processor: pluginVue.processors[".vue"], + rules: { + ...pluginVue.configs.recommended.rules, // Vue 鎺ㄨ崘瑙勫垯 + "vue/no-v-html": "off", // 鍏佽 v-html + "vue/multi-word-component-names": "off", // 鍏佽鍗曚釜鍗曡瘝缁勪欢鍚� + }, + }, +]; diff --git a/index.html b/index.html new file mode 100644 index 0000000..447fe80 --- /dev/null +++ b/index.html @@ -0,0 +1,58 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" href="/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="description" content="vue-element-template锛寁ue-element-admin 鐨勫紑鍙戠函鍑�妯℃澘" /> + <meta + name="keywords" + content="vue,element-plus,typescript,vue-element-admin,vue3-element-admin" + /> + <title>杞欢绠$悊骞冲彴</title> + </head> + + <body> + <div id="app"> + <div class="loader"></div> + </div> + </body> + <script type="module" src="/src/main.ts"></script> + + <style> + html, + body, + #app { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + .loader { + --d: 22px; + + width: 4px; + height: 4px; + color: #25b09b; + border-radius: 50%; + box-shadow: + calc(1 * var(--d)) calc(0 * var(--d)) 0 0, + calc(0.707 * var(--d)) calc(0.707 * var(--d)) 0 1px, + calc(0 * var(--d)) calc(1 * var(--d)) 0 2px, + calc(-0.707 * var(--d)) calc(0.707 * var(--d)) 0 3px, + calc(-1 * var(--d)) calc(0 * var(--d)) 0 4px, + calc(-0.707 * var(--d)) calc(-0.707 * var(--d)) 0 5px, + calc(0 * var(--d)) calc(-1 * var(--d)) 0 6px; + animation: l27 1s infinite steps(8); + } + + @keyframes l27 { + 100% { + transform: rotate(1turn); + } + } + </style> +</html> diff --git a/licenses/vue-element-admin/LICENSE b/licenses/vue-element-admin/LICENSE new file mode 100644 index 0000000..3fce384 --- /dev/null +++ b/licenses/vue-element-admin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present PanJiaChen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/licenses/vue3-element-admin/LICENSE b/licenses/vue3-element-admin/LICENSE new file mode 100644 index 0000000..9825cba --- /dev/null +++ b/licenses/vue3-element-admin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present 鏈夋潵寮�婧愮粍缁� + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mock/auth.mock.ts b/mock/auth.mock.ts new file mode 100644 index 0000000..59803e5 --- /dev/null +++ b/mock/auth.mock.ts @@ -0,0 +1,43 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "auth/captcha", + method: ["GET"], + body: { + code: "00000", + data: { + captchaKey: "534b8ef2b0a24121bec76391ddd159f9", + captchaBase64: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAAkCAIAAADNSmkJAAAFKUlEQVR4Xu2ZXUwcVRiGV70wMWo08V5NvPXCrDbFaGpMaZW2hqQxaoiJTRsaMBCNSYtpa2JTKiFSelFa+Q/QZcMWqEhBlh+htbEpZhMrBQrlJ0hBywLLyrJ0WZbje3bqOvPNLHPWrDvdOE9ONmfe78zkzMs335wzWJhJQrBQweS/wTQ6QWgYHdoIOcecOe05O+t2WkutO+p2ZF3Ksg/YV9ZW6FATYajR3nveg60H9327r3O8c35lHgp+r05dPdJzBL73TPSQ8SaCKIxGLsPlop+K0JHrEkPuoT31e5qGmmjARACF0agYyGVNlyVm/pzZXrN9fHGcBkz0UBid+31u93i3XFFT80vN8cvHqWqih8Lo1NpUqS5vwh3vnd223VQ10UNh9NbyrcFQUK6oCawHUipSqGqiB83oBf+CXFGDMp1mS6OqiR4Ko7FexkpOrqhpHGw82nOUqiZ6KIzGrkRuorW0dJMmOy+hOCfYGzb2RBFv6HRO0gEJw/U7y+pgL1bwmTxexN6sZ31TdEwEhdG+gA+7EqyXpUO1uZH20cWL8hMTRt1N9tBXzCJrOIRoCPJpSO2RAp4HmtCdIfZ+2JWgEBN9LbR28seTGU0Zue1tMLp+YIAMSADzfvbkKX4/eb28j4YODiGin3heqmIlLja5hAUCu+nmGY3JWKvpMAlqNGgebsauBOvlqSX+JEx7p7EbTLen53XlzfmWUioqXikrc68Y8N2juJ/fyVsNChGHEE//rBANYWaZz+TRQqpLaBgNsPfDrgSpbS21YtV87IdjrlkX9JZbt5DOma2t9ITo5F+5glN22WwL/n+yDv00mw06orKxOqQ5+J04hhViwzAXETIcJDVm8uxZqktoGx2Nj9t43Wgaul/ERQiGQvtbWnDWgZYW9CXlQFjZ/7ciyHNn+Z2MexTimIeLz59TiIln0M1e+IbPpOAaDUnEYPTi6iqKxpbycs/qKo1tCslfKcffPn9enuMiPPY1vxO/ckeFQ4h46cdGqUWoidE/y54q5tPY5WDrGzQqIXot4BgchEE57e00IMCw2/1qZSVO/7SjA78o9INzcxsbrL+fnTnDDh9mmZn8F30oG1Hm+nABv5mQMopDS/h1HxtqTzWbABMe9sxpPoe9zezeOo1GELqWhPS8t46M0IAYHbdvR1aHbaOjbjfLz2eFhez6dba4yAfgF30o0BFVE8+Mjh/wFxPI+I5mAEHU6Ls+38vhTFwOBGhMDF8gkFpbC5ffsdv/uBs6dIj19dExEtARVXv9YNbop8NFY3aZ6gRRo+tu3IBHnzmdNCBMXldXJKPfL74WzWUJRE+coDUknqsOdZXQbAJYwluVTbOZI3Qt8GFzMwxyjo3RgBiN4fr+elXVpZGRLWXl6PdOTtJBSlBDUK/lnIrjOlrtqWYTQDJaF6FrTXu9sOa1ysrVoM5HVE1GFxZQcyJ/p+xzv6K/rbr6N6+XDpUBl0tKFIrbz78qWB6YnWFMCBld4XLBms+7df75ook/GNzb0GCV7U1Qfz9p64TyQWNjYD3qe9rj4SMJtQP3MyjSDPzWIRHPjH7X4YAvfXoPuyZf9Pbi3PcuXIh4mp3NllYC6XY79C+jl2o8PBipxjnBttn4MgMNnWgfcRJGPI2OL8hTj3LloIlmRicvBhiNykvecpqoa3RSY4DRcLAwyicuOepVR1JjgNFYHWONHL04czTX0UmNAUYD7Pr+xc4wqTHGaBb2OtZvHUmNYUazcA2J6etdUmOk0f8rTKMTxF91RG0D1SwYGwAAAABJRU5ErkJggg==", + }, + msg: "涓�鍒噊k", + }, + }, + + { + url: "auth/login", + method: ["POST"], + body: { + code: "00000", + data: { + accessToken: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImRlcHRJZCI6MSwiZGF0YVNjb3BlIjoxLCJ1c2VySWQiOjIsImlhdCI6MTcyODE5MzA1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhZDg3NzlhZDZlYWY0OWY3OTE4M2ZmYmI5OWM4MjExMSJ9.58YHwL3sNNC22jyAmOZeSm-7MITzfHb_epBIz7LvWeA", + tokenType: "Bearer", + refreshToken: null, + expires: null, + }, + msg: "涓�鍒噊k", + }, + }, + + { + url: "auth/logout", + method: ["DELETE"], + body: { + code: "00000", + data: {}, + msg: "string", + }, + }, +]); diff --git a/mock/base.ts b/mock/base.ts new file mode 100644 index 0000000..438e1c1 --- /dev/null +++ b/mock/base.ts @@ -0,0 +1,10 @@ +import path from "path"; +import { createDefineMock } from "vite-plugin-mock-dev-server"; + +export const defineMock = createDefineMock((mock) => { + // 鎷兼帴url + mock.url = path.join( + import.meta.env.VITE_APP_BASE_API + "/api/v1/", + mock.url + ); +}); diff --git a/mock/dept.mock.ts b/mock/dept.mock.ts new file mode 100644 index 0000000..e9b6d1b --- /dev/null +++ b/mock/dept.mock.ts @@ -0,0 +1,153 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "dept/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: 1, + label: "鏈夋潵鎶�鏈�", + children: [ + { + value: 2, + label: "鐮斿彂閮ㄩ棬", + }, + { + value: 3, + label: "娴嬭瘯閮ㄩ棬", + }, + ], + }, + ], + msg: "涓�鍒噊k", + }, + }, + + { + url: "dept", + method: ["GET"], + body: { + code: "00000", + data: [ + { + id: 1, + parentId: 0, + name: "鏈夋潵鎶�鏈�", + code: "YOULAI", + sort: 1, + status: 1, + children: [ + { + id: 2, + parentId: 1, + name: "鐮斿彂閮ㄩ棬", + code: "RD001", + sort: 1, + status: 1, + children: [], + createTime: null, + updateTime: "2022-04-19 12:46", + }, + { + id: 3, + parentId: 1, + name: "娴嬭瘯閮ㄩ棬", + code: "QA001", + sort: 1, + status: 1, + children: [], + createTime: null, + updateTime: "2022-04-19 12:46", + }, + ], + createTime: null, + updateTime: null, + }, + ], + msg: "涓�鍒噊k", + }, + }, + + // 鏂板閮ㄩ棬 + { + url: "dept", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板閮ㄩ棬" + body.name + "鎴愬姛", + }; + }, + }, + + // 鑾峰彇閮ㄩ棬琛ㄥ崟鏁版嵁 + { + url: "dept/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: deptMap[params.id], + msg: "涓�鍒噊k", + }; + }, + }, + + // 淇敼閮ㄩ棬 + { + url: "dept/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼閮ㄩ棬" + body.name + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎閮ㄩ棬 + { + url: "dept/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎閮ㄩ棬" + params.id + "鎴愬姛", + }; + }, + }, +]); + +// 閮ㄩ棬鏄犲皠琛ㄦ暟鎹� +const deptMap: Record<string, any> = { + 1: { + id: 1, + name: "鏈夋潵鎶�鏈�", + code: "YOULAI", + parentId: 0, + status: 1, + sort: 1, + }, + 2: { + id: 2, + name: "鐮斿彂閮ㄩ棬", + code: "RD001", + parentId: 1, + status: 1, + sort: 1, + }, + 3: { + id: 3, + name: "娴嬭瘯閮ㄩ棬", + code: "QA001", + parentId: 1, + status: 1, + sort: 1, + }, +}; diff --git a/mock/dict.mock.ts b/mock/dict.mock.ts new file mode 100644 index 0000000..9eff20e --- /dev/null +++ b/mock/dict.mock.ts @@ -0,0 +1,307 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "dicts/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 1, + name: "鎬у埆", + dictCode: "gender", + status: 1, + }, + ], + total: 1, + }, + msg: "涓�鍒噊k", + }, + }, + + // 鏂板瀛楀吀 + { + url: "dicts", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板瀛楀吀" + body.name + "鎴愬姛", + }; + }, + }, + + // 鑾峰彇瀛楀吀琛ㄥ崟鏁版嵁 + { + url: "dicts/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: dictMap[params.id], + msg: "涓�鍒噊k", + }; + }, + }, + + // 淇敼瀛楀吀 + { + url: "dicts/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼瀛楀吀" + body.name + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎瀛楀吀 + { + url: "dicts/:ids", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎瀛楀吀" + params.ids + "鎴愬姛", + }; + }, + }, + + //--------------------------------------------------- + // 瀛楀吀椤圭浉鍏虫帴鍙� + //--------------------------------------------------- + + // 瀛楀吀椤瑰垎椤靛垪琛� + { + url: "dicts/:dictCode/items/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 1, + dictCode: "gender", + label: "鐢�", + value: "1", + sort: 1, + status: 1, + }, + { + id: 2, + dictCode: "gender", + label: "濂�", + value: "2", + sort: 2, + status: 1, + }, + { + id: 3, + dictCode: "gender", + label: "淇濆瘑", + value: "0", + sort: 3, + status: 1, + }, + ], + total: 3, + }, + msg: "涓�鍒噊k", + }, + }, + // 瀛楀吀椤瑰垪琛� + { + url: "dicts/:dictCode/items", + method: ["GET"], + body: ({ params }) => { + const dictCode = params.dictCode; + + let list = null; + + if (dictCode == "gender") { + list = [ + { + value: "1", + label: "鐢�", + }, + { + value: "2", + label: "濂�", + }, + { + value: "0", + label: "淇濆瘑", + }, + ]; + } else if (dictCode == "notice_level") { + list = [ + { + value: "L", + label: "浣�", + tag: "info", + }, + { + value: "M", + label: "涓�", + tag: "warning", + }, + { + value: "H", + label: "楂�", + tag: "danger", + }, + ]; + } else if (dictCode == "notice_type") { + list = [ + { + value: "1", + label: "绯荤粺鍗囩骇", + tag: "success", + }, + { + value: "2", + label: "绯荤粺缁存姢", + tag: "primary", + }, + { + value: "3", + label: "瀹夊叏璀﹀憡", + tag: "danger", + }, + { + value: "4", + label: "鍋囨湡閫氱煡", + tag: "success", + }, + { + value: "5", + label: "鍏徃鏂伴椈", + tag: "primary", + }, + { + value: "99", + label: "鍏朵粬", + tag: "info", + }, + ]; + } + + return { + code: "00000", + data: list, + msg: "涓�鍒噊k", + }; + }, + }, + // 鏂板瀛楀吀椤� + { + url: "dicts/:dictCode/items", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板瀛楀吀" + body.name + "鎴愬姛", + }; + }, + }, + + // 瀛楀吀椤硅〃鍗曟暟鎹� + { + url: "dicts/:dictCode/items/:itemId/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: dictItemMap[params.itemId], + msg: "涓�鍒噊k", + }; + }, + }, + + // 淇敼瀛楀吀椤� + { + url: "dicts/:dictCode/items/:itemId", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼瀛楀吀椤�" + body.name + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎瀛楀吀 + { + url: "dicts/:dictCode/items/:itemId", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎瀛楀吀" + params.itemId + "鎴愬姛", + }; + }, + }, +]); + +// 瀛楀吀鏄犲皠琛ㄦ暟鎹� +const dictMap: Record<string, any> = { + 1: { + code: "00000", + data: { + id: 1, + name: "鎬у埆", + dictCode: "gender", + status: 1, + }, + msg: "涓�鍒噊k", + }, +}; + +// 瀛楀吀椤规槧灏勮〃鏁版嵁 +const dictItemMap: Record<string, any> = { + 1: { + code: "00000", + data: { + id: 1, + value: "1", + label: "鐢�", + sort: 1, + status: 1, + tagType: "primary", + }, + msg: "涓�鍒噊k", + }, + 2: { + code: "00000", + data: { + id: 2, + value: "2", + label: "濂�", + sort: 2, + status: 1, + tagType: "danger", + }, + msg: "涓�鍒噊k", + }, + 3: { + code: "00000", + data: { + id: 3, + value: "0", + label: "淇濆瘑", + sort: 3, + status: 1, + tagType: "info", + }, + msg: "涓�鍒噊k", + }, +}; diff --git a/mock/menu.mock.ts b/mock/menu.mock.ts new file mode 100644 index 0000000..8879cb3 --- /dev/null +++ b/mock/menu.mock.ts @@ -0,0 +1,1285 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "menus/routes", + method: ["GET"], + body: { + code: "00000", + data: [ + { + path: "/system", + component: "Layout", + redirect: "/system/user", + name: "/system", + meta: { + title: "绯荤粺绠$悊", + icon: "system", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "user", + component: "system/user/index", + name: "User", + meta: { + title: "鐢ㄦ埛绠$悊", + icon: "el-icon-User", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "role", + component: "system/role/index", + name: "Role", + meta: { + title: "瑙掕壊绠$悊", + icon: "role", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "menu", + component: "system/menu/index", + name: "SysMenu", + meta: { + title: "鑿滃崟绠$悊", + icon: "menu", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dept", + component: "system/dept/index", + name: "Dept", + meta: { + title: "閮ㄩ棬绠$悊", + icon: "tree", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dict", + component: "system/dict/index", + name: "Dict", + meta: { + title: "瀛楀吀绠$悊", + icon: "dict", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dict-data", + component: "system/dict/data", + name: "DictItem", + meta: { + title: "瀛楀吀鏁版嵁", + icon: "", + hidden: true, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/multi-level", + component: "Layout", + name: "/multiLevel", + meta: { + title: "澶氱骇鑿滃崟", + icon: "cascader", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "multi-level1", + component: "demo/multi-level/level1", + name: "MultiLevel1", + meta: { + title: "鑿滃崟涓�绾�", + icon: "", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "multi-level2", + component: "demo/multi-level/children/level2", + name: "MultiLevel2", + meta: { + title: "鑿滃崟浜岀骇", + icon: "", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "multi-level3-1", + component: "demo/multi-level/children/children/level3-1", + name: "MultiLevel31", + meta: { + title: "鑿滃崟涓夌骇-1", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "multi-level3-2", + component: "demo/multi-level/children/children/level3-2", + name: "MultiLevel32", + meta: { + title: "鑿滃崟涓夌骇-2", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + ], + }, + ], + }, + { + path: "/api", + component: "Layout", + name: "/api", + meta: { + title: "鎺ュ彛鏂囨。", + icon: "api", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "apifox", + component: "demo/api/apifox", + name: "Apifox", + meta: { + title: "Apifox", + icon: "api", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/doc", + component: "Layout", + redirect: "https://juejin.cn/post/7228990409909108793", + name: "/doc", + meta: { + title: "骞冲彴鏂囨。", + icon: "document", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "internal-doc", + component: "demo/internal-doc", + name: "InternalDoc", + meta: { + title: "骞冲彴鏂囨。(鍐呭祵)", + icon: "document", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + { + path: "https://juejin.cn/post/7228990409909108793", + name: "Https://juejin.cn/post/7228990409909108793", + meta: { + title: "骞冲彴鏂囨。(澶栭摼)", + icon: "el-icon-link", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + ], + }, + ], + msg: "涓�鍒噊k", + }, + }, + + // 鑾峰彇鑿滃崟鏍戝舰鍒楄〃 + { + url: "menus", + method: ["GET"], + body: { + code: "00000", + data: [ + { + id: 1, + parentId: 0, + name: "绯荤粺绠$悊", + type: "CATALOG", + routeName: "", + routePath: "/system", + component: "Layout", + sort: 1, + visible: 1, + icon: "system", + redirect: "/system/user", + perm: null, + children: [ + { + id: 2, + parentId: 1, + name: "鐢ㄦ埛绠$悊", + type: "MENU", + routeName: "User", + routePath: "user", + component: "system/user/index", + sort: 1, + visible: 1, + icon: "el-icon-User", + redirect: null, + perm: null, + children: [ + { + id: 105, + parentId: 2, + name: "鐢ㄦ埛鏌ヨ", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 0, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:query", + children: [], + }, + { + id: 31, + parentId: 2, + name: "鐢ㄦ埛鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:add", + children: [], + }, + { + id: 32, + parentId: 2, + name: "鐢ㄦ埛缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:edit", + children: [], + }, + { + id: 33, + parentId: 2, + name: "鐢ㄦ埛鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:delete", + children: [], + }, + { + id: 88, + parentId: 2, + name: "閲嶇疆瀵嗙爜", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:password:reset", + children: [], + }, + { + id: 106, + parentId: 2, + name: "鐢ㄦ埛瀵煎叆", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:import", + children: [], + }, + { + id: 107, + parentId: 2, + name: "鐢ㄦ埛瀵煎嚭", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 6, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:export", + children: [], + }, + ], + }, + { + id: 3, + parentId: 1, + name: "瑙掕壊绠$悊", + type: "MENU", + routeName: "Role", + routePath: "role", + component: "system/role/index", + sort: 2, + visible: 1, + icon: "role", + redirect: null, + perm: null, + children: [ + { + id: 70, + parentId: 3, + name: "瑙掕壊鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:add", + children: [], + }, + { + id: 71, + parentId: 3, + name: "瑙掕壊缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:edit", + children: [], + }, + { + id: 72, + parentId: 3, + name: "瑙掕壊鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:delete", + children: [], + }, + ], + }, + { + id: 4, + parentId: 1, + name: "鑿滃崟绠$悊", + type: "MENU", + routeName: "Menu", + routePath: "menu", + component: "system/menu/index", + sort: 3, + visible: 1, + icon: "menu", + redirect: null, + perm: null, + children: [ + { + id: 73, + parentId: 4, + name: "鑿滃崟鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:add", + children: [], + }, + { + id: 74, + parentId: 4, + name: "鑿滃崟缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:edit", + children: [], + }, + { + id: 75, + parentId: 4, + name: "鑿滃崟鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:delete", + children: [], + }, + ], + }, + { + id: 5, + parentId: 1, + name: "閮ㄩ棬绠$悊", + type: "MENU", + routeName: "Dept", + routePath: "dept", + component: "system/dept/index", + sort: 4, + visible: 1, + icon: "tree", + redirect: null, + perm: null, + children: [ + { + id: 76, + parentId: 5, + name: "閮ㄩ棬鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:add", + children: [], + }, + { + id: 77, + parentId: 5, + name: "閮ㄩ棬缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:edit", + children: [], + }, + { + id: 78, + parentId: 5, + name: "閮ㄩ棬鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:delete", + children: [], + }, + ], + }, + { + id: 6, + parentId: 1, + name: "瀛楀吀绠$悊", + type: "MENU", + routeName: "Dict", + routePath: "dict", + component: "system/dict/index", + sort: 5, + visible: 1, + icon: "dict", + redirect: null, + perm: null, + children: [ + { + id: 79, + parentId: 6, + name: "瀛楀吀鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict:add", + children: [], + }, + { + id: 81, + parentId: 6, + name: "瀛楀吀缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict_type:edit", + children: [], + }, + { + id: 84, + parentId: 6, + name: "瀛楀吀鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict_type:delete", + children: [], + }, + ], + }, + { + id: 135, + parentId: 1, + name: "瀛楀吀鏁版嵁", + type: "MENU", + routeName: "DictData", + routePath: "dict-data", + component: "system/dict/data", + sort: 6, + visible: 0, + icon: "", + redirect: null, + perm: null, + children: [ + { + id: 136, + parentId: 135, + name: "瀛楀吀鏁版嵁鏂板", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-data:add", + children: [], + }, + { + id: 137, + parentId: 135, + name: "瀛楀吀鏁版嵁缂栬緫", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-data:edit", + children: [], + }, + { + id: 138, + parentId: 135, + name: "瀛楀吀鏁版嵁鍒犻櫎", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 6, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-data:delete", + children: [], + }, + ], + }, + ], + }, + { + id: 40, + parentId: 0, + name: "鎺ュ彛鏂囨。", + type: "CATALOG", + routeName: null, + routePath: "/api", + component: "Layout", + sort: 7, + visible: 1, + icon: "api", + redirect: "", + perm: null, + children: [ + { + id: 41, + parentId: 40, + name: "Apifox", + type: "MENU", + routeName: null, + routePath: "apifox", + component: "demo/api/apifox", + sort: 1, + visible: 1, + icon: "api", + redirect: "", + perm: null, + children: [], + }, + ], + }, + { + id: 26, + parentId: 0, + name: "骞冲彴鏂囨。", + type: "CATALOG", + routeName: null, + routePath: "/doc", + component: "Layout", + sort: 8, + visible: 1, + icon: "document", + redirect: "https://juejin.cn/post/7228990409909108793", + perm: null, + children: [ + { + id: 102, + parentId: 26, + name: "骞冲彴鏂囨。(鍐呭祵)", + type: "EXTLINK", + routeName: null, + routePath: "internal-doc", + component: "demo/internal-doc", + sort: 1, + visible: 1, + icon: "document", + redirect: "", + perm: null, + children: [], + }, + { + id: 30, + parentId: 26, + name: "骞冲彴鏂囨。(澶栭摼)", + type: "EXTLINK", + routeName: null, + routePath: "https://juejin.cn/post/7228990409909108793", + component: "", + sort: 2, + visible: 1, + icon: "link", + redirect: "", + perm: null, + children: [], + }, + ], + }, + { + id: 20, + parentId: 0, + name: "澶氱骇鑿滃崟", + type: "CATALOG", + routeName: null, + routePath: "/multi-level", + component: "Layout", + sort: 9, + visible: 1, + icon: "cascader", + redirect: "", + perm: null, + children: [ + { + id: 21, + parentId: 20, + name: "鑿滃崟涓�绾�", + type: "MENU", + routeName: null, + routePath: "multi-level1", + component: "demo/multi-level/level1", + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [ + { + id: 22, + parentId: 21, + name: "鑿滃崟浜岀骇", + type: "MENU", + routeName: null, + routePath: "multi-level2", + component: "demo/multi-level/children/level2", + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: null, + children: [ + { + id: 23, + parentId: 22, + name: "鑿滃崟涓夌骇-1", + type: "MENU", + routeName: null, + routePath: "multi-level3-1", + component: "demo/multi-level/children/children/level3-1", + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 24, + parentId: 22, + name: "鑿滃崟涓夌骇-2", + type: "MENU", + routeName: null, + routePath: "multi-level3-2", + component: "demo/multi-level/children/children/level3-2", + sort: 2, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + msg: "涓�鍒噊k", + }, + }, + + // 鑾峰彇鑿滃崟鏍戝舰涓嬫媺鍒楄〃 + { + url: "menus/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: "1", + label: "绯荤粺绠$悊", + children: [ + { + value: "2", + label: "鐢ㄦ埛绠$悊", + children: [ + { + value: "105", + label: "鐢ㄦ埛鏌ヨ", + }, + { + value: "31", + label: "鐢ㄦ埛鏂板", + }, + { + value: "32", + label: "鐢ㄦ埛缂栬緫", + }, + { + value: "33", + label: "鐢ㄦ埛鍒犻櫎", + }, + { + value: "88", + label: "閲嶇疆瀵嗙爜", + }, + { + value: "106", + label: "鐢ㄦ埛瀵煎叆", + }, + { + value: "107", + label: "鐢ㄦ埛瀵煎嚭", + }, + ], + }, + { + value: "3", + label: "瑙掕壊绠$悊", + children: [ + { + value: "139", + label: "瑙掕壊鏌ヨ", + }, + { + value: "70", + label: "瑙掕壊鏂板", + }, + { + value: "71", + label: "瑙掕壊缂栬緫", + }, + { + value: "72", + label: "瑙掕壊鍒犻櫎", + }, + ], + }, + { + value: "4", + label: "鑿滃崟绠$悊", + children: [ + { + value: "73", + label: "鑿滃崟鏂板", + }, + { + value: "140", + label: "鑿滃崟鏌ヨ", + }, + { + value: "75", + label: "鑿滃崟鍒犻櫎", + }, + { + value: "74", + label: "鑿滃崟缂栬緫", + }, + ], + }, + { + value: "5", + label: "閮ㄩ棬绠$悊", + children: [ + { + value: "76", + label: "閮ㄩ棬鏂板", + }, + { + value: "141", + label: "閮ㄩ棬鏌ヨ", + }, + { + value: "77", + label: "閮ㄩ棬缂栬緫", + }, + { + value: "78", + label: "閮ㄩ棬鍒犻櫎", + }, + ], + }, + { + value: "6", + label: "瀛楀吀绠$悊", + children: [ + { + value: "79", + label: "瀛楀吀鏂板", + }, + { + value: "142", + label: "瀛楀吀鏌ヨ", + }, + { + value: "81", + label: "瀛楀吀缂栬緫", + }, + { + value: "84", + label: "瀛楀吀鍒犻櫎", + }, + ], + }, + { + value: "117", + label: "绯荤粺鏃ュ織", + }, + { + value: "135", + label: "瀛楀吀鏁版嵁", + children: [ + { + value: "143", + label: "瀛楀吀鏁版嵁鏌ヨ", + }, + { + value: "136", + label: "瀛楀吀鏁版嵁鏂板", + }, + { + value: "137", + label: "瀛楀吀鏁版嵁缂栬緫", + }, + { + value: "138", + label: "瀛楀吀鏁版嵁鍒犻櫎", + }, + ], + }, + { + value: "120", + label: "绯荤粺閰嶇疆", + children: [ + { + value: "121", + label: "绯荤粺閰嶇疆鏌ヨ", + }, + { + value: "122", + label: "绯荤粺閰嶇疆鏂板", + }, + { + value: "123", + label: "绯荤粺閰嶇疆淇敼", + }, + { + value: "124", + label: "绯荤粺閰嶇疆鍒犻櫎", + }, + { + value: "125", + label: "绯荤粺閰嶇疆鍒锋柊", + }, + ], + }, + { + value: "126", + label: "閫氱煡鍏憡", + children: [ + { + value: "127", + label: "閫氱煡鏌ヨ", + }, + { + value: "128", + label: "閫氱煡鏂板", + }, + { + value: "129", + label: "閫氱煡缂栬緫", + }, + { + value: "130", + label: "閫氱煡鍒犻櫎", + }, + { + value: "133", + label: "閫氱煡鍙戝竷", + }, + { + value: "134", + label: "閫氱煡鎾ゅ洖", + }, + ], + }, + ], + }, + { + value: "118", + label: "绯荤粺宸ュ叿", + children: [ + { + value: "119", + label: "浠g爜鐢熸垚", + }, + ], + }, + { + value: "40", + label: "鎺ュ彛鏂囨。", + children: [ + { + value: "41", + label: "Apifox", + }, + ], + }, + { + value: "26", + label: "骞冲彴鏂囨。", + children: [ + { + value: "102", + label: "document", + }, + { + value: "30", + label: "骞冲彴鏂囨。(澶栭摼)", + }, + ], + }, + { + value: "20", + label: "澶氱骇鑿滃崟", + children: [ + { + value: "21", + label: "鑿滃崟涓�绾�", + children: [ + { + value: "22", + label: "鑿滃崟浜岀骇", + children: [ + { + value: "23", + label: "鑿滃崟涓夌骇-1", + }, + { + value: "24", + label: "鑿滃崟涓夌骇-2", + }, + ], + }, + ], + }, + ], + }, + ], + + msg: "涓�鍒噊k", + }, + }, + + // 鏂板鑿滃崟 + { + url: "menus", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板鑿滃崟" + body.name + "鎴愬姛", + }; + }, + }, + + // 鑾峰彇鑿滃崟琛ㄥ崟鏁版嵁 + { + url: "menus/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: menuMap[params.id], + msg: "涓�鍒噊k", + }; + }, + }, + + // 淇敼鑿滃崟 + { + url: "menus/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼鑿滃崟" + body.name + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎鑿滃崟 + { + url: "menus/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎鑿滃崟" + params.id + "鎴愬姛", + }; + }, + }, +]); + +// 鑿滃崟鏄犲皠琛ㄦ暟鎹� +const menuMap: Record<string, any> = { + 1: { + id: 1, + parentId: 0, + name: "绯荤粺绠$悊", + type: "CATALOG", + routeName: "", + routePath: "/system", + component: "Layout", + perm: null, + visible: 1, + sort: 1, + icon: "system", + redirect: "/system/user", + keepAlive: null, + alwaysShow: null, + params: null, + }, + 2: { + id: 2, + parentId: 1, + name: "鐢ㄦ埛绠$悊", + type: "MENU", + routeName: "User", + routePath: "user", + component: "system/user/index", + perm: null, + visible: 1, + sort: 1, + icon: "user", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 3: { + id: 3, + parentId: 1, + name: "瑙掕壊绠$悊", + type: "MENU", + routeName: "Role", + routePath: "role", + component: "system/role/index", + perm: null, + visible: 1, + sort: 2, + icon: "role", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 4: { + id: 4, + parentId: 1, + name: "鑿滃崟绠$悊", + type: "MENU", + routeName: "Menu", + routePath: "menu", + component: "system/menu/index", + perm: null, + visible: 1, + sort: 3, + icon: "menu", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 5: { + id: 5, + parentId: 1, + name: "閮ㄩ棬绠$悊", + type: "MENU", + routeName: "Dept", + routePath: "dept", + component: "system/dept/index", + perm: null, + visible: 1, + sort: 4, + icon: "tree", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 6: { + id: 6, + parentId: 1, + name: "瀛楀吀绠$悊", + type: "MENU", + routeName: "Dict", + routePath: "dict", + component: "system/dict/index", + perm: null, + visible: 1, + sort: 5, + icon: "dict", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, +}; diff --git a/mock/role.mock.ts b/mock/role.mock.ts new file mode 100644 index 0000000..cc87026 --- /dev/null +++ b/mock/role.mock.ts @@ -0,0 +1,335 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "roles/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: 2, + label: "绯荤粺绠$悊鍛�", + }, + { + value: 4, + label: "绯荤粺绠$悊鍛�1", + }, + { + value: 5, + label: "绯荤粺绠$悊鍛�2", + }, + { + value: 6, + label: "绯荤粺绠$悊鍛�3", + }, + { + value: 7, + label: "绯荤粺绠$悊鍛�4", + }, + { + value: 8, + label: "绯荤粺绠$悊鍛�5", + }, + { + value: 9, + label: "绯荤粺绠$悊鍛�6", + }, + { + value: 10, + label: "绯荤粺绠$悊鍛�7", + }, + { + value: 11, + label: "绯荤粺绠$悊鍛�8", + }, + { + value: 12, + label: "绯荤粺绠$悊鍛�9", + }, + { + value: 3, + label: "璁块棶娓稿", + }, + ], + msg: "涓�鍒噊k", + }, + }, + + { + url: "roles/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 2, + name: "绯荤粺绠$悊鍛�", + code: "ADMIN", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 3, + name: "璁块棶娓稿", + code: "GUEST", + status: 1, + sort: 3, + createTime: "2021-05-26 15:49:05", + updateTime: "2019-05-05 16:00:00", + }, + { + id: 4, + name: "绯荤粺绠$悊鍛�1", + code: "ADMIN1", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 5, + name: "绯荤粺绠$悊鍛�2", + code: "ADMIN2", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 6, + name: "绯荤粺绠$悊鍛�3", + code: "ADMIN3", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 7, + name: "绯荤粺绠$悊鍛�4", + code: "ADMIN4", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 8, + name: "绯荤粺绠$悊鍛�5", + code: "ADMIN5", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 9, + name: "绯荤粺绠$悊鍛�6", + code: "ADMIN6", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: "2023-12-04 11:43:15", + }, + { + id: 10, + name: "绯荤粺绠$悊鍛�7", + code: "ADMIN7", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 11, + name: "绯荤粺绠$悊鍛�8", + code: "ADMIN8", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + ], + total: 10, + }, + msg: "涓�鍒噊k", + }, + }, + + // 鏂板瑙掕壊 + { + url: "roles", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板瑙掕壊" + body.name + "鎴愬姛", + }; + }, + }, + + // 鑾峰彇瑙掕壊琛ㄥ崟鏁版嵁 + { + url: "roles/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: roleMap[params.id], + msg: "涓�鍒噊k", + }; + }, + }, + // 淇敼瑙掕壊 + { + url: "roles/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼瑙掕壊" + body.name + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎瑙掕壊 + { + url: "roles/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎瑙掕壊" + params.id + "鎴愬姛", + }; + }, + }, + // 鑾峰彇瑙掕壊鎷ユ湁鐨勮彍鍗旾D + { + url: "roles/:id/menuIds", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: [ + 1, 2, 31, 32, 33, 88, 3, 70, 71, 72, 4, 73, 75, 74, 5, 76, 77, 78, 6, + 79, 81, 84, 85, 86, 87, 40, 41, 26, 30, 20, 21, 22, 23, 24, 89, 90, + 91, 36, 37, 38, 39, 93, 94, 95, 97, 102, 89, 90, 91, 93, 94, 95, 97, + 102, 103, 104, + ], + msg: "涓�鍒噊k", + }; + }, + }, + // 淇濆瓨瑙掕壊鑿滃崟 + { + url: "roles/:id/menus", + method: ["PUT"], + body: { + code: "00000", + data: null, + msg: "涓�鍒噊k", + }, + }, +]); + +// 瑙掕壊鏄犲皠琛ㄦ暟鎹� +const roleMap: Record<string, any> = { + 2: { + id: 2, + name: "绯荤粺绠$悊鍛�", + code: "ADMIN", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 3: { + id: 3, + name: "璁块棶娓稿", + code: "GUEST", + status: 1, + sort: 3, + createTime: "2021-05-26 15:49:05", + updateTime: "2019-05-05 16:00:00", + }, + 4: { + id: 4, + name: "绯荤粺绠$悊鍛�1", + code: "ADMIN1", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 5: { + id: 5, + name: "绯荤粺绠$悊鍛�2", + code: "ADMIN2", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + + 6: { + id: 6, + name: "绯荤粺绠$悊鍛�3", + code: "ADMIN3", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 7: { + id: 7, + name: "绯荤粺绠$悊鍛�4", + code: "ADMIN4", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 8: { + id: 8, + name: "绯荤粺绠$悊鍛�5", + code: "ADMIN5", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 9: { + id: 9, + name: "绯荤粺绠$悊鍛�6", + code: "ADMIN6", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: "2023-12-04 11:43:15", + }, + 10: { + id: 10, + name: "绯荤粺绠$悊鍛�7", + code: "ADMIN7", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 11: { + id: 11, + name: "绯荤粺绠$悊鍛�8", + code: "ADMIN8", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, +}; diff --git a/mock/user.mock.ts b/mock/user.mock.ts new file mode 100644 index 0000000..e82a2e1 --- /dev/null +++ b/mock/user.mock.ts @@ -0,0 +1,245 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "users/me", + method: ["GET"], + body: { + code: "00000", + data: { + userId: 2, + username: "admin", + nickname: "绯荤粺绠$悊鍛�", + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + roles: ["ADMIN"], + perms: [ + "sys:notice:edit", + "sys:menu:delete", + "sys:dict:edit", + "sys:notice:query", + "sys:dict:delete", + "sys:config:add", + "sys:config:refresh", + "sys:menu:add", + "sys:user:add", + "sys:user:export", + "sys:role:edit", + "sys:dept:delete", + "sys:config:update", + "sys:user:password:reset", + "sys:notice:revoke", + "sys:user:import", + "sys:user:delete", + "sys:dict_type:delete", + "sys:dict:add", + "sys:role:add", + "sys:notice:publish", + "sys:notice:delete", + "sys:dept:edit", + "sys:dict_type:edit", + "sys:user:query", + "sys:user:edit", + "sys:config:delete", + "sys:dept:add", + "sys:notice:add", + "sys:role:delete", + "sys:menu:edit", + "sys:config:query", + ], + }, + msg: "涓�鍒噊k", + }, + }, + + { + url: "users/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 2, + username: "admin", + nickname: "绯荤粺绠$悊鍛�", + mobile: "17621210366", + gender: 1, + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "", + status: 1, + deptId: 1, + roleIds: [2], + }, + { + id: 3, + username: "test", + nickname: "娴嬭瘯灏忕敤鎴�", + mobile: "17621210366", + gender: 1, + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "youlaitech@163.com", + status: 1, + deptId: 3, + roleIds: [3], + }, + ], + total: 2, + }, + msg: "涓�鍒噊k", + }, + }, + + // 鏂板鐢ㄦ埛 + { + url: "users", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "鏂板鐢ㄦ埛" + body.nickname + "鎴愬姛", + }; + }, + }, + + // 鑾峰彇鐢ㄦ埛琛ㄥ崟鏁版嵁 + { + url: "users/:userId/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: userMap[params.userId], + msg: "涓�鍒噊k", + }; + }, + }, + // 淇敼鐢ㄦ埛 + { + url: "users/:userId", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "淇敼鐢ㄦ埛" + body.nickname + "鎴愬姛", + }; + }, + }, + + // 鍒犻櫎鐢ㄦ埛 + { + url: "users/:userId", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "鍒犻櫎鐢ㄦ埛" + params.id + "鎴愬姛", + }; + }, + }, + + // 閲嶇疆瀵嗙爜 + { + url: "users/:userId/password/reset", + method: ["PUT"], + body({ query }) { + return { + code: "00000", + data: null, + msg: "閲嶇疆瀵嗙爜鎴愬姛锛屾柊瀵嗙爜涓猴細" + query.password, + }; + }, + }, + + // 瀵煎嚭Excel + { + url: "users/_export", + method: ["GET"], + headers: { + "Content-Disposition": + "attachment; filename=%E7%94%A8%E6%88%B7%E5%88%97%E8%A1%A8.xlsx", + "Content-Type": + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + }, + + { + url: "users/profile", + method: ["GET"], + body: { + code: "00000", + data: { + id: 2, + username: "admin", + nickname: "绯荤粺绠$悊鍛�", + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + gender: 1, + mobile: "17621210366", + email: null, + deptName: "鏈夋潵鎶�鏈�", + roleNames: "绯荤粺绠$悊鍛�", + createTime: "2019-10-10", + }, + }, + }, + + { + url: "users/profile", + method: ["PUT"], + body({ query }) { + return { + code: "00000", + data: null, + msg: "淇敼涓汉淇℃伅鎴愬姛", + }; + }, + }, + + { + url: "users/password", + method: ["PUT"], + body({ query }) { + return { + code: "00000", + data: null, + msg: "淇敼瀵嗙爜鎴愬姛", + }; + }, + }, +]); + +// 鐢ㄦ埛鏄犲皠琛ㄦ暟鎹� +const userMap: Record<string, any> = { + 2: { + id: 2, + username: "admin", + nickname: "绯荤粺绠$悊鍛�", + mobile: "17621210366", + gender: 1, + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "", + status: 1, + deptId: 1, + roleIds: [2], + }, + 3: { + id: 3, + username: "test", + nickname: "娴嬭瘯灏忕敤鎴�", + mobile: "17621210366", + gender: 1, + avatar: + "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "youlaitech@163.com", + status: 1, + deptId: 3, + roleIds: [3], + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..29a21ec --- /dev/null +++ b/package.json @@ -0,0 +1,122 @@ +{ + "name": "vue3-element-template", + "version": "2.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit & vite build", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit", + "lint:eslint": "eslint --cache \"src/**/*.{vue,ts}\" --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"", + "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix", + "lint:lint-staged": "lint-staged", + "preinstall": "npx only-allow pnpm", + "prepare": "husky", + "commit": "git-cz" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "lint-staged": { + "*.{js,ts}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json}": [ + "prettier --write" + ], + "*.{vue,html}": [ + "eslint --fix", + "prettier --write", + "stylelint --fix" + ], + "*.{scss,css}": [ + "stylelint --fix", + "prettier --write" + ], + "*.md": [ + "prettier --write" + ] + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@vueuse/core": "^12.8.2", + "@wangeditor-next/editor": "^5.6.34", + "@wangeditor-next/editor-for-vue": "^5.1.14", + "animate.css": "^4.1.1", + "axios": "^1.8.2", + "default-passive-events": "^2.0.0", + "echarts": "^5.6.0", + "element-plus": "^2.9.6", + "js-md5": "^0.8.3", + "jsencrypt": "^3.3.2", + "lodash-es": "^4.17.21", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "path-browserify": "^1.0.1", + "path-to-regexp": "^8.2.0", + "pinia": "^3.0.1", + "qs": "^6.14.0", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "@eslint/js": "^9.22.0", + "@iconify/utils": "^2.3.0", + "@types/codemirror": "^5.60.15", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.13.10", + "@types/nprogress": "^0.2.3", + "@types/path-browserify": "^1.0.3", + "@types/qs": "^6.9.18", + "@types/sortablejs": "^1.15.8", + "@typescript-eslint/eslint-plugin": "^8.26.1", + "@typescript-eslint/parser": "^8.26.1", + "@vitejs/plugin-vue": "^5.2.1", + "autoprefixer": "^10.4.21", + "commitizen": "^4.3.1", + "cz-git": "^1.11.1", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-vue": "^10.0.0", + "globals": "^15.15.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "postcss": "^8.5.3", + "postcss-html": "^1.8.0", + "postcss-scss": "^4.0.9", + "prettier": "^3.5.3", + "sass": "^1.85.1", + "stylelint": "^16.15.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^6.0.0", + "stylelint-config-recommended": "^15.0.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.6.0", + "stylelint-prettier": "^5.0.3", + "terser": "^5.39.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1", + "unocss": "65.4.3", + "unplugin-auto-import": "^19.1.1", + "unplugin-vue-components": "^28.4.1", + "vite": "^6.2.1", + "vite-plugin-mock-dev-server": "^1.8.4", + "vue-eslint-parser": "^10.1.1", + "vue-tsc": "^2.2.8" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", + "author": "鏈夋潵寮�婧愮粍缁�", + "license": "MIT" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..5e82ccc --- /dev/null +++ b/public/favicon.ico Binary files differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..3a0e3b6 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,35 @@ +<template> + <el-config-provider :locale="locale" :size="size"> + <!-- 寮�鍚按鍗� --> + <el-watermark + v-if="watermarkEnabled" + :font="{ color: fontColor }" + :content="defaultSettings.watermarkContent" + :z-index="9999" + class="wh-full" + > + <router-view /> + </el-watermark> + <!-- 鍏抽棴姘村嵃 --> + <router-view v-else /> + </el-config-provider> +</template> + +<script setup lang="ts"> +import { useAppStore, useSettingsStore } from "@/store"; +import defaultSettings from "@/settings"; +import { ThemeMode } from "@/enums/settings/theme.enum"; +import { ComponentSize } from "@/enums/settings/layout.enum"; + +const appStore = useAppStore(); +const settingsStore = useSettingsStore(); + +const locale = computed(() => appStore.locale); +const size = computed(() => appStore.size as ComponentSize); +const watermarkEnabled = computed(() => settingsStore.watermarkEnabled); + +// 鏄庝寒/鏆楅粦涓婚姘村嵃瀛椾綋棰滆壊閫傞厤 +const fontColor = computed(() => { + return settingsStore.theme === ThemeMode.DARK ? "rgba(255, 255, 255, .15)" : "rgba(0, 0, 0, .15)"; +}); +</script> diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts new file mode 100644 index 0000000..a38a783 --- /dev/null +++ b/src/api/auth.api.ts @@ -0,0 +1,82 @@ +import request from "@/utils/request"; + +const AUTH_BASE_URL = "/api/v1/auth"; + +const AuthAPI = { + /** 鐧诲綍鎺ュ彛*/ + login(data: LoginFormData) { + const formData = new FormData(); + formData.append("username", data.username); + formData.append("password", data.password); + formData.append("captchaKey", data.captchaKey); + formData.append("captchaCode", data.captchaCode); + return request<any, LoginResult>({ + url: `${AUTH_BASE_URL}/login`, + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** 鍒锋柊 token 鎺ュ彛*/ + refreshToken(refreshToken: string) { + return request<any, LoginResult>({ + url: `${AUTH_BASE_URL}/refresh-token`, + method: "post", + data: { refreshToken: refreshToken }, + headers: { + Authorization: "no-auth", + }, + }); + }, + + /** 娉ㄩ攢鎺ュ彛*/ + logout() { + return request({ + url: `${AUTH_BASE_URL}/logout`, + method: "delete", + }); + }, + + /** 鑾峰彇楠岃瘉鐮佹帴鍙�*/ + getCaptcha() { + return request<any, CaptchaResult>({ + url: `${AUTH_BASE_URL}/captcha`, + method: "get", + }); + }, +}; + +export default AuthAPI; + +/** 鐧诲綍璇锋眰鍙傛暟 */ +export interface LoginFormData { + /** 鐢ㄦ埛鍚� */ + username: string; + /** 瀵嗙爜 */ + password: string; + /** 楠岃瘉鐮佺紦瀛榢ey */ + captchaKey: string; + /** 楠岃瘉鐮� */ + captchaCode: string; +} + +/** 鐧诲綍鍝嶅簲 */ +export interface LoginResult { + /** 璁块棶浠ょ墝 */ + accessToken: string; + /** 浠ょ墝绫诲瀷 */ + tokenType: string; + /** 杩囨湡鏃堕棿(绉�) */ + expiresIn: number; +} + +/** 楠岃瘉鐮佸搷搴� */ +export interface CaptchaResult { + /** 楠岃瘉鐮佺紦瀛榢ey */ + captchaKey: string; + /** 楠岃瘉鐮佸浘鐗嘊ase64瀛楃涓� */ + captchaBase64: string; +} diff --git a/src/api/file.api.ts b/src/api/file.api.ts new file mode 100644 index 0000000..24a480f --- /dev/null +++ b/src/api/file.api.ts @@ -0,0 +1,81 @@ +import request from "@/utils/request"; + +const FileAPI = { + /** + * 涓婁紶鏂囦欢 + * + * @param file + */ + upload(formData: FormData) { + return request<any, FileInfo>({ + url: "/api/v1/files", + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** + * 涓婁紶鏂囦欢 + */ + uploadFile(file: File) { + const formData = new FormData(); + formData.append("file", file); + return request<any, FileInfo>({ + url: "/api/v1/files", + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** + * 鍒犻櫎鏂囦欢 + * + * @param filePath 鏂囦欢瀹屾暣璺緞 + */ + delete(filePath?: string) { + return request({ + url: "/api/v1/files", + method: "delete", + params: { filePath: filePath }, + }); + }, + + /** + * 涓嬭浇鏂囦欢 + * @param url + * @param fileName + */ + download(url: string, fileName?: string) { + return request({ + url: url, + method: "get", + responseType: "blob", + }).then((res) => { + const blob = new Blob([res.data]); + const a = document.createElement("a"); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName || "涓嬭浇鏂囦欢"; + a.click(); + window.URL.revokeObjectURL(url); + }); + }, +}; + +export default FileAPI; + +/** + * 鏂囦欢API绫诲瀷澹版槑 + */ +export interface FileInfo { + /** 鏂囦欢鍚� */ + name: string; + /** 鏂囦欢璺緞 */ + url: string; +} diff --git a/src/api/software.ts b/src/api/software.ts new file mode 100644 index 0000000..23b93f9 --- /dev/null +++ b/src/api/software.ts @@ -0,0 +1,83 @@ +import request from "@/utils/request"; +import type { ResultV2, ResultV3 } from "@/types/request-result"; + +const SoftwareApi = { + search(data: SearchParams) { + let params = JSON.parse(JSON.stringify(data)); + if (!params.materialCode) { + delete params.materialCode; + } + + if (!params.snCode) { + delete params.snCode; + } + + if (!params.serialNumber) { + delete params.serialNumber; + } + + return request<any, ResultV2<SoftwareInfo>>({ + method: "GET", + url: "software/getPage", + params, + }); + }, + + searchSn() { + return request<any, ResultV3<string>>({ + method: "GET", + url: "software/getSnCodeList", + }); + }, + + searchMaterial() { + return request<any, ResultV3<string>>({ + method: "GET", + url: "software/getMaterialCodeList", + }); + }, + + searchSerial() { + return request<any, ResultV3<string>>({ + method: "GET", + url: "software/getSerialNumberList", + }); + }, + + uploadSoftware(data: FormData) { + return request<any, UploadResult>({ + method: "POST", + url: "software/upload", + headers: { + "Content-Type": "multipart/form-data" + }, + data, + }); + }, +}; +export default SoftwareApi; + +export interface SoftwareInfo { + id: number; + snCode: string; + serialNumber: string; + materialCode: string; + fileName: string; + fileUrl: string; + uploadUserId: string; + uploadUserName: string; + createTime: string; +} + +export interface SearchParams { + snCode?: string; + materialCode?: string; + serialNumber?: string; + pageNum: number; + pageSize: number; +} + +export interface UploadResult { + code: number; + msg: string; +} diff --git a/src/api/system/dept.api.ts b/src/api/system/dept.api.ts new file mode 100644 index 0000000..200065d --- /dev/null +++ b/src/api/system/dept.api.ts @@ -0,0 +1,130 @@ +import request from "@/utils/request"; + +const DEPT_BASE_URL = "/api/v1/dept"; + +const DeptAPI = { + /** + * 鑾峰彇閮ㄩ棬鍒楄〃 + * + * @param queryParams 鏌ヨ鍙傛暟锛堝彲閫夛級 + * @returns 閮ㄩ棬鏍戝舰琛ㄦ牸鏁版嵁 + */ + getList(queryParams?: DeptQuery) { + return request<any, DeptVO[]>({ + url: `${DEPT_BASE_URL}`, + method: "get", + params: queryParams, + }); + }, + + /** 鑾峰彇閮ㄩ棬涓嬫媺鍒楄〃 */ + getOptions() { + return request<any, OptionType[]>({ + url: `${DEPT_BASE_URL}/options`, + method: "get", + }); + }, + + /** + * 鑾峰彇閮ㄩ棬琛ㄥ崟鏁版嵁 + * + * @param id 閮ㄩ棬ID + * @returns 閮ㄩ棬琛ㄥ崟鏁版嵁 + */ + getFormData(id: string) { + return request<any, DeptForm>({ + url: `${DEPT_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 鏂板閮ㄩ棬 + * + * @param data 閮ㄩ棬琛ㄥ崟鏁版嵁 + * @returns 璇锋眰缁撴灉 + */ + create(data: DeptForm) { + return request({ + url: `${DEPT_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 淇敼閮ㄩ棬 + * + * @param id 閮ㄩ棬ID + * @param data 閮ㄩ棬琛ㄥ崟鏁版嵁 + * @returns 璇锋眰缁撴灉 + */ + update(id: string, data: DeptForm) { + return request({ + url: `${DEPT_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 鍒犻櫎閮ㄩ棬 + * + * @param ids 閮ㄩ棬ID锛屽涓互鑻辨枃閫楀彿(,)鍒嗛殧 + * @returns 璇锋眰缁撴灉 + */ + deleteByIds(ids: string) { + return request({ + url: `${DEPT_BASE_URL}/${ids}`, + method: "delete", + }); + }, +}; + +export default DeptAPI; + +/** 閮ㄩ棬鏌ヨ鍙傛暟 */ +export interface DeptQuery { + /** 鎼滅储鍏抽敭瀛� */ + keywords?: string; + /** 鐘舵�� */ + status?: number; +} + +/** 閮ㄩ棬绫诲瀷 */ +export interface DeptVO { + /** 瀛愰儴闂� */ + children?: DeptVO[]; + /** 鍒涘缓鏃堕棿 */ + createTime?: Date; + /** 閮ㄩ棬ID */ + id?: string; + /** 閮ㄩ棬鍚嶇О */ + name?: string; + /** 閮ㄩ棬缂栧彿 */ + code?: string; + /** 鐖堕儴闂↖D */ + parentid?: string; + /** 鎺掑簭 */ + sort?: number; + /** 鐘舵��(1:鍚敤锛�0:绂佺敤) */ + status?: number; + /** 淇敼鏃堕棿 */ + updateTime?: Date; +} + +/** 閮ㄩ棬琛ㄥ崟绫诲瀷 */ +export interface DeptForm { + /** 閮ㄩ棬ID(鏂板涓嶅~) */ + id?: string; + /** 閮ㄩ棬鍚嶇О */ + name?: string; + /** 閮ㄩ棬缂栧彿 */ + code?: string; + /** 鐖堕儴闂↖D */ + parentId: string; + /** 鎺掑簭 */ + sort?: number; + /** 鐘舵��(1:鍚敤锛�0锛氱鐢�) */ + status?: number; +} diff --git a/src/api/system/dict.api.ts b/src/api/system/dict.api.ts new file mode 100644 index 0000000..dc2c981 --- /dev/null +++ b/src/api/system/dict.api.ts @@ -0,0 +1,302 @@ +import request from "@/utils/request"; + +const DICT_BASE_URL = "/api/v1/dicts"; + +const DictAPI = { + //--------------------------------------------------- + // 瀛楀吀鐩稿叧鎺ュ彛 + //--------------------------------------------------- + + /** + * 瀛楀吀鍒嗛〉鍒楄〃 + * + * @param queryParams 鏌ヨ鍙傛暟 + * @returns 瀛楀吀鍒嗛〉缁撴灉 + */ + getPage(queryParams: DictPageQuery) { + return request<any, PageResult<DictPageVO[]>>({ + url: `${DICT_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 瀛楀吀琛ㄥ崟鏁版嵁 + * + * @param id 瀛楀吀ID + * @returns 瀛楀吀琛ㄥ崟鏁版嵁 + */ + getFormData(id: string) { + return request<any, ResponseData<DictForm>>({ + url: `${DICT_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 鏂板瀛楀吀 + * + * @param data 瀛楀吀琛ㄥ崟鏁版嵁 + */ + create(data: DictForm) { + return request({ + url: `${DICT_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 淇敼瀛楀吀 + * + * @param id 瀛楀吀ID + * @param data 瀛楀吀琛ㄥ崟鏁版嵁 + */ + update(id: string, data: DictForm) { + return request({ + url: `${DICT_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 鍒犻櫎瀛楀吀 + * + * @param ids 瀛楀吀ID锛屽涓互鑻辨枃閫楀彿(,)鍒嗛殧 + */ + deleteByIds(ids: string) { + return request({ + url: `${DICT_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + //--------------------------------------------------- + // 瀛楀吀椤圭浉鍏虫帴鍙� + //--------------------------------------------------- + /** + * 鑾峰彇瀛楀吀鍒嗛〉鍒楄〃 + * + * @param queryParams 鏌ヨ鍙傛暟 + * @returns 瀛楀吀鍒嗛〉缁撴灉 + */ + getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) { + return request<any, PageResult<DictItemPageVO[]>>({ + url: `${DICT_BASE_URL}/${dictCode}/items/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 鑾峰彇瀛楀吀椤瑰垪琛� + */ + getDictItems(dictCode: string) { + return request<any, DictItemOption[]>({ + url: `${DICT_BASE_URL}/${dictCode}/items`, + method: "get", + }); + }, + + /** + * 鏂板瀛楀吀椤� + */ + createDictItem(dictCode: string, data: DictItemForm) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items`, + method: "post", + data: data, + }); + }, + + /** + * 鑾峰彇瀛楀吀椤硅〃鍗曟暟鎹� + * + * @param id 瀛楀吀椤笽D + * @returns 瀛楀吀椤硅〃鍗曟暟鎹� + */ + getDictItemFormData(dictCode: string, id: string) { + return request<any, ResponseData<DictItemForm>>({ + url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`, + method: "get", + }); + }, + + /** + * 淇敼瀛楀吀椤� + */ + updateDictItem(dictCode: string, id: string, data: DictItemForm) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 鍒犻櫎瀛楀吀椤� + */ + deleteDictItems(dictCode: string, ids: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, + method: "delete", + }); + }, +}; + +export default DictAPI; + +/** + * 瀛楀吀鏌ヨ鍙傛暟 + */ +export interface DictPageQuery extends PageQuery { + /** + * 鍏抽敭瀛�(瀛楀吀鍚嶇О/缂栫爜) + */ + keywords?: string; + + /** + * 瀛楀吀鐘舵�侊紙1:鍚敤锛�0:绂佺敤锛� + */ + status?: number; +} + +/** + * 瀛楀吀鍒嗛〉瀵硅薄 + */ +export interface DictPageVO { + /** + * 瀛楀吀ID + */ + id: string; + /** + * 瀛楀吀鍚嶇О + */ + name: string; + /** + * 瀛楀吀缂栫爜 + */ + dictCode: string; + /** + * 瀛楀吀鐘舵�侊紙1:鍚敤锛�0:绂佺敤锛� + */ + status: number; +} + +/** + * 瀛楀吀 + */ +export interface DictForm { + /** + * 瀛楀吀ID + */ + id?: string; + /** + * 瀛楀吀鍚嶇О + */ + name?: string; + /** + * 瀛楀吀缂栫爜 + */ + dictCode?: string; + /** + * 瀛楀吀鐘舵�侊紙1-鍚敤锛�0-绂佺敤锛� + */ + status?: number; + /** + * 澶囨敞 + */ + remark?: string; +} + +/** + * 瀛楀吀鏌ヨ鍙傛暟 + */ +export interface DictItemPageQuery extends PageQuery { + /** 鍏抽敭瀛�(瀛楀吀鏁版嵁鍊�/鏍囩) */ + keywords?: string; + + /** 瀛楀吀缂栫爜 */ + dictCode?: string; +} + +/** + * 瀛楀吀鍒嗛〉瀵硅薄 + */ +export interface DictItemPageVO { + /** + * 瀛楀吀ID + */ + id: string; + /** + * 瀛楀吀缂栫爜 + */ + dictCode: string; + /** + * 瀛楀吀鏁版嵁鍊� + */ + value: string; + /** + * 瀛楀吀鏁版嵁鏍囩 + */ + label: string; + /** + * 鐘舵�侊紙1:鍚敤锛�0:绂佺敤) + */ + status: number; + /** + * 瀛楀吀鎺掑簭 + */ + sort?: number; +} + +/** + * 瀛楀吀 + */ +export interface DictItemForm { + /** + * 瀛楀吀ID + */ + id?: string; + /** + * 瀛楀吀缂栫爜 + */ + dictCode?: string; + /** + * 瀛楀吀鏁版嵁鍊� + */ + value?: string; + /** + * 瀛楀吀鏁版嵁鏍囩 + */ + label?: string; + /** + * 鐘舵�侊紙1:鍚敤锛�0:绂佺敤) + */ + status?: number; + /** + * 瀛楀吀鎺掑簭 + */ + sort?: number; + + /** + * 鏍囩绫诲瀷 + */ + tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined; +} + +/** + * 瀛楀吀椤逛笅鎷夐�夐」 + */ +export interface DictItemOption { + /** 瀛楀吀鏁版嵁鍊� */ + value: string; + + /** 瀛楀吀鏁版嵁鏍囩 */ + label: string; + + /** 鏍囩绫诲瀷 */ + tagType: string; +} diff --git a/src/api/system/menu.api.ts b/src/api/system/menu.api.ts new file mode 100644 index 0000000..0a7de38 --- /dev/null +++ b/src/api/system/menu.api.ts @@ -0,0 +1,207 @@ +import request from "@/utils/request"; +// 鑿滃崟鍩虹URL +const MENU_BASE_URL = "/api/v1/menus"; + +const MenuAPI = { + /** + * 鑾峰彇褰撳墠鐢ㄦ埛鐨勮矾鐢卞垪琛� + * <p/> + * 鏃犻渶浼犲叆瑙掕壊锛屽悗绔В鏋恡oken鑾峰彇瑙掕壊鑷鍒ゆ柇鏄惁鎷ユ湁璺敱鐨勬潈闄� + * + * @returns 璺敱鍒楄〃 + */ + getRoutes() { + return request<any, RouteVO[]>({ + url: `${MENU_BASE_URL}/routes`, + method: "get", + }); + }, + + /** + * 鑾峰彇鑿滃崟鏍戝舰鍒楄〃 + * + * @param queryParams 鏌ヨ鍙傛暟 + * @returns 鑿滃崟鏍戝舰鍒楄〃 + */ + getList(queryParams: MenuQuery) { + return request<any, MenuVO[]>({ + url: `${MENU_BASE_URL}`, + method: "get", + params: queryParams, + }); + }, + + /** + * 鑾峰彇鑿滃崟涓嬫媺鏁版嵁婧� + * + * @returns 鑿滃崟涓嬫媺鏁版嵁婧� + */ + getOptions(onlyParent?: boolean) { + return request<any, OptionType[]>({ + url: `${MENU_BASE_URL}/options`, + method: "get", + params: { onlyParent: onlyParent }, + }); + }, + + /** + * 鑾峰彇鑿滃崟琛ㄥ崟鏁版嵁 + * + * @param id 鑿滃崟ID + */ + getFormData(id: string) { + return request<any, MenuForm>({ + url: `${MENU_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 娣诲姞鑿滃崟 + * + * @param data 鑿滃崟琛ㄥ崟鏁版嵁 + * @returns 璇锋眰缁撴灉 + */ + create(data: MenuForm) { + return request({ + url: `${MENU_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 淇敼鑿滃崟 + * + * @param id 鑿滃崟ID + * @param data 鑿滃崟琛ㄥ崟鏁版嵁 + * @returns 璇锋眰缁撴灉 + */ + update(id: string, data: MenuForm) { + return request({ + url: `${MENU_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 鍒犻櫎鑿滃崟 + * + * @param id 鑿滃崟ID + * @returns 璇锋眰缁撴灉 + */ + deleteById(id: string) { + return request({ + url: `${MENU_BASE_URL}/${id}`, + method: "delete", + }); + }, +}; + +export default MenuAPI; + +/** 鑿滃崟鏌ヨ鍙傛暟 */ +export interface MenuQuery { + /** 鎼滅储鍏抽敭瀛� */ + keywords?: string; +} + +/** 鑿滃崟瑙嗗浘瀵硅薄 */ +export interface MenuVO { + /** 瀛愯彍鍗� */ + children?: MenuVO[]; + /** 缁勪欢璺緞 */ + component?: string; + /** ICON */ + icon?: string; + /** 鑿滃崟ID */ + id?: string; + /** 鑿滃崟鍚嶇О */ + name?: string; + /** 鐖惰彍鍗旾D */ + parentId?: string; + /** 鎸夐挳鏉冮檺鏍囪瘑 */ + perm?: string; + /** 璺宠浆璺緞 */ + redirect?: string; + /** 璺敱鍚嶇О */ + routeName?: string; + /** 璺敱鐩稿璺緞 */ + routePath?: string; + /** 鑿滃崟鎺掑簭(鏁板瓧瓒婂皬鎺掑悕瓒婇潬鍓�) */ + sort?: number; + /** 鑿滃崟 */ + type?: number; + /** 鑿滃崟鏄惁鍙(1:鏄剧ず;0:闅愯棌) */ + visible?: number; +} + +/** 鑿滃崟琛ㄥ崟瀵硅薄 */ +export interface MenuForm { + /** 鑿滃崟ID */ + id?: string; + /** 鐖惰彍鍗旾D */ + parentId?: string; + /** 鑿滃崟鍚嶇О */ + name?: string; + /** 鑿滃崟鏄惁鍙(1-鏄� 0-鍚�) */ + visible: number; + /** ICON */ + icon?: string; + /** 鎺掑簭 */ + sort?: number; + /** 璺敱鍚嶇О */ + routeName?: string; + /** 璺敱璺緞 */ + routePath?: string; + /** 缁勪欢璺緞 */ + component?: string; + /** 璺宠浆璺敱璺緞 */ + redirect?: string; + /** 鑿滃崟 */ + type?: number; + /** 鏉冮檺鏍囪瘑 */ + perm?: string; + /** 銆愯彍鍗曘�戞槸鍚﹀紑鍚〉闈㈢紦瀛� */ + keepAlive?: number; + /** 銆愮洰褰曘�戝彧鏈変竴涓瓙璺敱鏄惁濮嬬粓鏄剧ず */ + alwaysShow?: number; + /** 鍙傛暟 */ + params?: KeyValue[]; +} + +interface KeyValue { + key: string; + value: string; +} + +/** RouteVO锛岃矾鐢卞璞� */ +export interface RouteVO { + /** 瀛愯矾鐢卞垪琛� */ + children: RouteVO[]; + /** 缁勪欢璺緞 */ + component?: string; + /** 璺敱灞炴�� */ + meta?: Meta; + /** 璺敱鍚嶇О */ + name?: string; + /** 璺敱璺緞 */ + path?: string; + /** 璺宠浆閾炬帴 */ + redirect?: string; +} + +/** Meta锛岃矾鐢卞睘鎬� */ +export interface Meta { + /** 銆愮洰褰曘�戝彧鏈変竴涓瓙璺敱鏄惁濮嬬粓鏄剧ず */ + alwaysShow?: boolean; + /** 鏄惁闅愯棌(true-鏄� false-鍚�) */ + hidden?: boolean; + /** ICON */ + icon?: string; + /** 銆愯彍鍗曘�戞槸鍚﹀紑鍚〉闈㈢紦瀛� */ + keepAlive?: boolean; + /** 璺敱title */ + title?: string; +} diff --git a/src/api/system/role.api.ts b/src/api/system/role.api.ts new file mode 100644 index 0000000..b36448f --- /dev/null +++ b/src/api/system/role.api.ts @@ -0,0 +1,138 @@ +import request from "@/utils/request"; + +const ROLE_BASE_URL = "/api/v1/roles"; + +const RoleAPI = { + /** 鑾峰彇瑙掕壊鍒嗛〉鏁版嵁 */ + getPage(queryParams?: RolePageQuery) { + return request<any, PageResult<RolePageVO[]>>({ + url: `${ROLE_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** 鑾峰彇瑙掕壊涓嬫媺鏁版嵁婧� */ + getOptions() { + return request<any, OptionType[]>({ + url: `${ROLE_BASE_URL}/options`, + method: "get", + }); + }, + /** + * 鑾峰彇瑙掕壊鐨勮彍鍗旾D闆嗗悎 + * + * @param roleId 瑙掕壊ID + * @returns 瑙掕壊鐨勮彍鍗旾D闆嗗悎 + */ + getRoleMenuIds(roleId: string) { + return request<any, number[]>({ + url: `${ROLE_BASE_URL}/${roleId}/menuIds`, + method: "get", + }); + }, + + /** + * 鍒嗛厤鑿滃崟鏉冮檺 + * + * @param roleId 瑙掕壊ID + * @param data 鑿滃崟ID闆嗗悎 + */ + updateRoleMenus(roleId: string, data: number[]) { + return request({ + url: `${ROLE_BASE_URL}/${roleId}/menus`, + method: "put", + data: data, + }); + }, + + /** + * 鑾峰彇瑙掕壊琛ㄥ崟鏁版嵁 + * + * @param id 瑙掕壊ID + * @returns 瑙掕壊琛ㄥ崟鏁版嵁 + */ + getFormData(id: string) { + return request<any, RoleForm>({ + url: `${ROLE_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** 娣诲姞瑙掕壊 */ + create(data: RoleForm) { + return request({ + url: `${ROLE_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 鏇存柊瑙掕壊 + * + * @param id 瑙掕壊ID + * @param data 瑙掕壊琛ㄥ崟鏁版嵁 + */ + update(id: string, data: RoleForm) { + return request({ + url: `${ROLE_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 鎵归噺鍒犻櫎瑙掕壊锛屽涓互鑻辨枃閫楀彿(,)鍒嗗壊 + * + * @param ids 瑙掕壊ID瀛楃涓诧紝澶氫釜浠ヨ嫳鏂囬�楀彿(,)鍒嗗壊 + */ + deleteByIds(ids: string) { + return request({ + url: `${ROLE_BASE_URL}/${ids}`, + method: "delete", + }); + }, +}; + +export default RoleAPI; + +/** 瑙掕壊鍒嗛〉鏌ヨ鍙傛暟 */ +export interface RolePageQuery extends PageQuery { + /** 鎼滅储鍏抽敭瀛� */ + keywords?: string; +} + +/** 瑙掕壊鍒嗛〉瀵硅薄 */ +export interface RolePageVO { + /** 瑙掕壊缂栫爜 */ + code?: string; + /** 瑙掕壊ID */ + id?: string; + /** 瑙掕壊鍚嶇О */ + name?: string; + /** 鎺掑簭 */ + sort?: number; + /** 瑙掕壊鐘舵�� */ + status?: number; + /** 鍒涘缓鏃堕棿 */ + createTime?: Date; + /** 淇敼鏃堕棿 */ + updateTime?: Date; +} + +/** 瑙掕壊琛ㄥ崟瀵硅薄 */ +export interface RoleForm { + /** 瑙掕壊ID */ + id?: string; + /** 瑙掕壊缂栫爜 */ + code?: string; + /** 鏁版嵁鏉冮檺 */ + dataScope?: number; + /** 瑙掕壊鍚嶇О */ + name?: string; + /** 鎺掑簭 */ + sort?: number; + /** 瑙掕壊鐘舵��(1-姝e父锛�0-鍋滅敤) */ + status?: number; +} diff --git a/src/api/system/user.api.ts b/src/api/system/user.api.ts new file mode 100644 index 0000000..efa123d --- /dev/null +++ b/src/api/system/user.api.ts @@ -0,0 +1,378 @@ +import request from "@/utils/request"; + +const USER_BASE_URL = "/api/v1/users"; + +const UserAPI = { + /** + * 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅 + * + * @returns 鐧诲綍鐢ㄦ埛鏄电О銆佸ご鍍忎俊鎭紝鍖呮嫭瑙掕壊鍜屾潈闄� + */ + getInfo() { + return request<any, UserInfo>({ + url: `${USER_BASE_URL}/me`, + method: "get", + }); + }, + + /** + * 鑾峰彇鐢ㄦ埛鍒嗛〉鍒楄〃 + * + * @param queryParams 鏌ヨ鍙傛暟 + */ + getPage(queryParams: UserPageQuery) { + return request<any, PageResult<UserPageVO[]>>({ + url: `${USER_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 鑾峰彇鐢ㄦ埛琛ㄥ崟璇︽儏 + * + * @param userId 鐢ㄦ埛ID + * @returns 鐢ㄦ埛琛ㄥ崟璇︽儏 + */ + getFormData(userId: string) { + return request<any, UserForm>({ + url: `${USER_BASE_URL}/${userId}/form`, + method: "get", + }); + }, + + /** + * 娣诲姞鐢ㄦ埛 + * + * @param data 鐢ㄦ埛琛ㄥ崟鏁版嵁 + */ + create(data: UserForm) { + return request({ + url: `${USER_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 淇敼鐢ㄦ埛 + * + * @param id 鐢ㄦ埛ID + * @param data 鐢ㄦ埛琛ㄥ崟鏁版嵁 + */ + update(id: string, data: UserForm) { + return request({ + url: `${USER_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 淇敼鐢ㄦ埛瀵嗙爜 + * + * @param id 鐢ㄦ埛ID + * @param password 鏂板瘑鐮� + */ + resetPassword(id: string, password: string) { + return request({ + url: `${USER_BASE_URL}/${id}/password/reset`, + method: "put", + params: { password: password }, + }); + }, + + /** + * 鎵归噺鍒犻櫎鐢ㄦ埛锛屽涓互鑻辨枃閫楀彿(,)鍒嗗壊 + * + * @param ids 鐢ㄦ埛ID瀛楃涓诧紝澶氫釜浠ヨ嫳鏂囬�楀彿(,)鍒嗗壊 + */ + deleteByIds(ids: string) { + return request({ + url: `${USER_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + /** 涓嬭浇鐢ㄦ埛瀵煎叆妯℃澘 */ + downloadTemplate() { + return request({ + url: `${USER_BASE_URL}/template`, + method: "get", + responseType: "blob", + }); + }, + + /** + * 瀵煎嚭鐢ㄦ埛 + * + * @param queryParams 鏌ヨ鍙傛暟 + */ + export(queryParams: UserPageQuery) { + return request({ + url: `${USER_BASE_URL}/export`, + method: "get", + params: queryParams, + responseType: "blob", + }); + }, + + /** + * 瀵煎叆鐢ㄦ埛 + * + * @param file 鏂囦欢 + */ + import(file: File) { + const formData = new FormData(); + formData.append("file", file); + return request<any, ExcelResult>({ + url: `${USER_BASE_URL}/import`, + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** 鑾峰彇涓汉涓績鐢ㄦ埛淇℃伅 */ + getProfile() { + return request<any, UserProfileVO>({ + url: `${USER_BASE_URL}/profile`, + method: "get", + }); + }, + + /** 淇敼涓汉涓績鐢ㄦ埛淇℃伅 */ + updateProfile(data: UserProfileForm) { + return request({ + url: `${USER_BASE_URL}/profile`, + method: "put", + data: data, + }); + }, + + /** 淇敼涓汉涓績鐢ㄦ埛瀵嗙爜 */ + changePassword(data: PasswordChangeForm) { + return request({ + url: `${USER_BASE_URL}/password`, + method: "put", + data: data, + }); + }, + + /** + * 鍙戦�佹墜鏈�/閭楠岃瘉鐮� + * + * @param contact 鑱旂郴鏂瑰紡 鎵嬫満鍙�/閭 + * @param contactType 鑱旂郴鏂瑰紡绫诲瀷 MOBILE:鎵嬫満;EMAIL:閭 + */ + sendVerificationCode(contact: string, contactType: string) { + return request({ + url: `${USER_BASE_URL}/send-verification-code`, + method: "get", + params: { contact: contact, contactType: contactType }, + }); + }, + + /** 缁戝畾涓汉涓績鐢ㄦ埛鎵嬫満 */ + bindMobile(data: MobileBindingForm) { + return request({ + url: `${USER_BASE_URL}/mobile`, + method: "put", + data: data, + }); + }, + + /** 缁戝畾涓汉涓績鐢ㄦ埛閭 */ + bindEmail(data: EmailBindingForm) { + return request({ + url: `${USER_BASE_URL}/email`, + method: "put", + data: data, + }); + }, + + /** + * 鑾峰彇鐢ㄦ埛涓嬫媺鍒楄〃 + */ + getOptions() { + return request<any, OptionType[]>({ + url: `${USER_BASE_URL}/options`, + method: "get", + }); + }, +}; + +export default UserAPI; + +/** 鐧诲綍鐢ㄦ埛淇℃伅 */ +export interface UserInfo { + /** 鐢ㄦ埛ID */ + userid?: string; + + /** 鐢ㄦ埛鍚� */ + username?: string; + + /** 鏄电О */ + nickname?: string; + + /** 澶村儚URL */ + avatar?: string; + + /** 瑙掕壊 */ + roles: string[]; + + /** 鏉冮檺 */ + perms: string[]; +} + +/** + * 鐢ㄦ埛鍒嗛〉鏌ヨ瀵硅薄 + */ +export interface UserPageQuery extends PageQuery { + /** 鎼滅储鍏抽敭瀛� */ + keywords?: string; + + /** 鐢ㄦ埛鐘舵�� */ + status?: number; + + /** 閮ㄩ棬ID */ + deptId?: string; + + /** 寮�濮嬫椂闂� */ + createTime?: [string, string]; +} + +/** 鐢ㄦ埛鍒嗛〉瀵硅薄 */ +export interface UserPageVO { + /** 鐢ㄦ埛ID */ + id: string; + /** 鐢ㄦ埛澶村儚URL */ + avatar?: string; + /** 鍒涘缓鏃堕棿 */ + createTime?: Date; + /** 閮ㄩ棬鍚嶇О */ + deptName?: string; + /** 鐢ㄦ埛閭 */ + email?: string; + /** 鎬у埆 */ + gender?: number; + /** 鎵嬫満鍙� */ + mobile?: string; + /** 鐢ㄦ埛鏄电О */ + nickname?: string; + /** 瑙掕壊鍚嶇О锛屽涓娇鐢ㄨ嫳鏂囬�楀彿(,)鍒嗗壊 */ + roleNames?: string; + /** 鐢ㄦ埛鐘舵��(1:鍚敤;0:绂佺敤) */ + status?: number; + /** 鐢ㄦ埛鍚� */ + username?: string; +} + +/** 鐢ㄦ埛琛ㄥ崟绫诲瀷 */ +export interface UserForm { + /** 鐢ㄦ埛澶村儚 */ + avatar?: string; + /** 閮ㄩ棬ID */ + deptId?: string; + /** 閭 */ + email?: string; + /** 鎬у埆 */ + gender?: number; + /** 鐢ㄦ埛ID */ + id?: string; + /** 鎵嬫満鍙� */ + mobile?: string; + /** 鏄电О */ + nickname?: string; + /** 瑙掕壊ID闆嗗悎 */ + roleIds?: number[]; + /** 鐢ㄦ埛鐘舵��(1:姝e父;0:绂佺敤) */ + status?: number; + /** 鐢ㄦ埛鍚� */ + username?: string; +} + +/** 涓汉涓績鐢ㄦ埛淇℃伅 */ +export interface UserProfileVO { + /** 鐢ㄦ埛ID */ + id?: string; + + /** 鐢ㄦ埛鍚� */ + username?: string; + + /** 鏄电О */ + nickname?: string; + + /** 澶村儚URL */ + avatar?: string; + + /** 鎬у埆 */ + gender?: number; + + /** 鎵嬫満鍙� */ + mobile?: string; + + /** 閭 */ + email?: string; + + /** 閮ㄩ棬鍚嶇О */ + deptName?: string; + + /** 瑙掕壊鍚嶇О锛屽涓娇鐢ㄨ嫳鏂囬�楀彿(,)鍒嗗壊 */ + roleNames?: string; + + /** 鍒涘缓鏃堕棿 */ + createTime?: Date; +} + +/** 涓汉涓績鐢ㄦ埛淇℃伅琛ㄥ崟 */ +export interface UserProfileForm { + /** 鐢ㄦ埛ID */ + id?: string; + + /** 鐢ㄦ埛鍚� */ + username?: string; + + /** 鏄电О */ + nickname?: string; + + /** 澶村儚URL */ + avatar?: string; + + /** 鎬у埆 */ + gender?: number; + + /** 鎵嬫満鍙� */ + mobile?: string; + + /** 閭 */ + email?: string; +} + +/** 淇敼瀵嗙爜琛ㄥ崟 */ +export interface PasswordChangeForm { + /** 鍘熷瘑鐮� */ + oldPassword?: string; + /** 鏂板瘑鐮� */ + newPassword?: string; + /** 纭鏂板瘑鐮� */ + confirmPassword?: string; +} + +/** 淇敼鎵嬫満琛ㄥ崟 */ +export interface MobileBindingForm { + /** 鎵嬫満鍙� */ + mobile?: string; + /** 楠岃瘉鐮� */ + code?: string; +} + +/** 淇敼閭琛ㄥ崟 */ +export interface EmailBindingForm { + /** 閭 */ + email?: string; + /** 楠岃瘉鐮� */ + code?: string; +} diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..d4692ec --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,127 @@ +import request from "@/utils/request"; +import { type ResultV1, ResultV4 } from "@/types/request-result"; +const UserApi = { + /** + * 鐧诲綍 + * @param name 鐢ㄦ埛鍚� + * @param pwd 瀵嗙爜 + */ + login(name: string, pwd: string) { + return request<any, LoginResult>({ + method: "POST", + url: "login/login", + params: { + name, + pwd, + }, + }); + }, + + // 瀹夊叏閫�鍑� + logout() { + return request({ + method: "POST", + url: "login/logout", + }); + }, + + /** + * 鏌ヨ鐢ㄦ埛-甯﹀垎椤� + * @param pageNum 褰撳墠椤� + * @param pageSize 姣忛〉鏉℃暟 + */ + search(pageNum: number, pageSize: number) { + return request<any, ResultV1<UserInfo>>({ + method: "GET", + url: "user/getPage", + params: { + pageNum, + pageSize, + }, + }); + }, + + /** + * 娣诲姞鐢ㄦ埛淇℃伅 + * @param data + */ + add(data: UserInfo) { + return request<any, ResultV4>({ + method: "POST", + url: "user/add", + data, + }); + }, + + /** + * 缂栬緫鐢ㄦ埛淇℃伅 + * @param data + */ + edit(data: UserInfo) { + return request<any, ResultV4>({ + method: "POST", + url: "user/update", + data, + }); + }, +}; + +export default UserApi; + +/** 鐧诲綍璇锋眰鍙傛暟 */ +export interface LoginFormData { + /** 鐢ㄦ埛鍚� */ + username: string; + /** 瀵嗙爜 */ + password: string; + /** 楠岃瘉鐮佺紦瀛榢ey */ + captchaKey: string; + /** 楠岃瘉鐮� */ + captchaCode: string; +} + +/** 鐧诲綍鐢ㄦ埛淇℃伅 */ +export interface UserInfo { + /** 鐢ㄦ埛ID */ + id?: number; + + /** 鐢ㄦ埛鍚� */ + name?: string; + + /** 瀵嗙爜 */ + pwd?: string; + + /** 鎬у埆 */ + sex?: number; + + /** 瑙掕壊 */ + roleId?: number; + + /** 閭 */ + email?: string; + + /** 鐢佃瘽 */ + phoneNumber?: string; + + /** 鍒涘缓鏃ユ湡 */ + createTime?: string; + + roles: string[]; + + perms: string[]; +} + +/** 鐧诲綍鎺ュ彛杩斿洖 */ +export interface LoginResult { + /** 鎺ュ彛鏄惁璋冪敤鎴愬姛 */ + code: number; + + /** 鏄惁鐧诲綍鎴愬姛 */ + data: boolean; + + /** 杩斿洖缁撴灉 */ + data2?: UserInfo; + + /** 杩斿洖鎻愮ず */ + msg: string; +} diff --git a/src/assets/const/const_user.ts b/src/assets/const/const_user.ts new file mode 100644 index 0000000..29cbd10 --- /dev/null +++ b/src/assets/const/const_user.ts @@ -0,0 +1,45 @@ +export const sexList: Sex[] = [ + { + label: "鐢�", + value: 1, + }, + { + label: "濂�", + value: 2, + }, +]; + +export const roleList: Role[] = [ + { + label: "瀹㈡埛", + value: 1, + roles: ['customer'], + perms: [] + }, + { + label: "鍚庡彴绠$悊", + value: 2, + roles: ['admin'], + perms: [ + "sys:user:add", + "sys:user:edit", + "sys:user:delete", + "sys:user:upload", + ] + } +]; + + +export interface Sex { + label: string; + value: number | string; +} + +export interface Role { + label: string; + value: number; + roles: string[]; + perms: string[]; +} + + diff --git a/src/assets/icons/api.svg b/src/assets/icons/api.svg new file mode 100644 index 0000000..0181bdd --- /dev/null +++ b/src/assets/icons/api.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M499.2 671.232v-261.12h102.4c16.384 0 28.672 1.024 37.888 2.56 13.312 2.048 24.576 6.656 34.816 13.312 9.728 6.656 17.92 16.384 23.552 28.16 6.144 12.288 8.704 25.6 8.192 38.4 0 23.552-7.68 44.032-23.04 59.904-15.36 16.896-40.96 25.088-78.848 25.088h-43.52v93.184l-61.44.512zm281.6 0h-61.952v-261.12H780.8v261.12zm-287.744 0h-69.12L396.8 601.6h-73.728l-25.088 69.632h-66.56l100.352-261.12h54.272l107.008 261.12zM343.552 545.28h32.256l-15.872-42.496c0-.512-.512-1.024-.512-1.536l-15.872 44.032zm217.6-26.112h43.52c20.48 0 28.16-4.608 31.232-7.168 4.608-4.096 7.168-10.752 7.168-18.944 0-6.656-1.536-11.776-4.096-15.36-2.56-3.584-6.144-6.144-10.752-7.68-1.536-.512-6.656-1.536-24.064-1.536h-43.008v50.688z"/><path d="M747.52 842.752H512c-8.704 0-16.384-3.584-22.016-9.728-6.144-6.144-9.216-14.336-8.704-22.528.512-16.896 14.336-30.72 31.232-31.232H747.52c115.712 0 209.408-94.208 209.408-209.408 0-104.96-78.848-194.56-183.296-207.872l-22.528-3.072-4.608-22.016C724.992 231.936 631.808 156.16 524.288 156.16c-124.928 0-226.304 101.376-226.304 226.304v8.704l1.536 36.352-36.352-4.096c-6.144-1.024-12.288-1.024-18.432-1.024-98.304 0-178.176 79.872-178.176 178.176 0 98.304 79.872 178.176 178.176 178.176h63.488c8.704 0 16.384 3.584 22.016 9.728 6.144 6.144 9.216 14.336 8.704 22.528-.512 16.896-14.336 30.72-31.232 31.232h-64c-64 0-123.904-25.088-169.472-70.144C28.16 726.528 3.072 665.6 3.072 601.088c0-129.536 103.936-236.544 232.448-241.152 12.288-157.184 149.504-276.48 307.2-266.24 59.904 3.584 118.784 27.136 165.888 65.536 45.568 37.376 77.824 87.04 94.208 143.872 125.952 26.112 217.088 137.728 217.088 266.752.512 151.04-121.856 272.896-272.384 272.896z"/><path d="M572.416 930.816c-8.192 0-15.872-3.072-21.504-8.704L431.616 812.544l113.152-117.76c6.144-6.144 13.824-9.216 22.528-9.216 8.704 0 16.384 3.072 22.528 9.216 11.776 11.776 12.288 31.232 1.024 44.032l-68.608 70.656 71.68 66.048c6.144 5.632 9.728 13.312 10.24 22.016.512 8.704-2.56 16.384-8.192 23.04-6.656 6.656-14.848 10.24-23.552 10.24z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/backtop.svg b/src/assets/icons/backtop.svg new file mode 100644 index 0000000..f8e6aa0 --- /dev/null +++ b/src/assets/icons/backtop.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg> \ No newline at end of file diff --git a/src/assets/icons/bilibili.svg b/src/assets/icons/bilibili.svg new file mode 100644 index 0000000..b86747c --- /dev/null +++ b/src/assets/icons/bilibili.svg @@ -0,0 +1 @@ +<svg t="1733556119022" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="60026" width="128" height="128"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#EC5D85" p-id="60027"></path><path d="M512 241.96096h52.224l65.06496-96.31744c49.63328-50.31936 89.64096 0.43008 63.85664 45.71136l-34.31424 51.5072c257.64864 5.02784 257.64864 43.008 257.64864 325.03808 0 325.94944 0 336.46592-404.48 336.46592S107.52 893.8496 107.52 567.90016c0-277.69856 0-318.80192 253.14304-324.95616l-39.43424-58.368c-31.26272-54.90688 37.33504-90.40896 64.68608-42.37312l60.416 99.80928c18.18624-0.0512 41.18528-0.0512 65.66912-0.0512z" fill="#EF85A7" p-id="60028"></path><path d="M512 338.5856c332.8 0 332.8 0 332.8 240.64s0 248.39168-332.8 248.39168-332.8-7.75168-332.8-248.39168 0-240.64 332.8-240.64z" fill="#EC5D85" p-id="60029"></path><path d="M281.6 558.08a30.72 30.72 0 0 1-27.47392-16.97792 30.72 30.72 0 0 1 13.73184-41.216l122.88-61.44a30.72 30.72 0 0 1 41.216 13.74208 30.72 30.72 0 0 1-13.74208 41.216l-122.88 61.44a30.59712 30.59712 0 0 1-13.73184 3.23584zM752.64 558.08a30.60736 30.60736 0 0 1-12.8512-2.83648l-133.12-61.44a30.72 30.72 0 0 1-15.04256-40.7552 30.72 30.72 0 0 1 40.76544-15.02208l133.12 61.44A30.72 30.72 0 0 1 752.64 558.08zM454.656 666.88a15.36 15.36 0 0 1-12.288-6.1952 15.36 15.36 0 0 1 3.072-21.49376l68.5056-50.91328 50.35008 52.62336a15.36 15.36 0 0 1-22.20032 21.23776l-31.5904-33.024-46.71488 34.72384a15.28832 15.28832 0 0 1-9.13408 3.04128z" fill="#EF85A7" p-id="60030"></path><path d="M65.536 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM362.25024 383.03744l34.816 303.17568h34.64192L405.23776 381.1328zM309.52448 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM446.86336 542.98624h45.80352V705.3312h-33.87392zM296.6016 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM326.99392 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM470.62016 459.88864h19.456v62.27968h-19.456zM440.23808 459.88864h22.20032v62.27968h-16.62976z" fill="#FFFFFF" p-id="60031"></path><path d="M243.56864 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z" fill="#EB5480" p-id="60032"></path><path d="M513.29024 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM810.00448 383.03744l34.816 303.17568h34.64192L852.992 381.1328zM757.27872 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM894.6176 542.98624h45.80352V705.3312H906.5472zM744.35584 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM774.74816 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM918.3744 459.88864h19.456v62.27968h-19.456zM887.99232 459.88864h22.20032v62.27968h-16.62976z" fill="#FFFFFF" p-id="60033"></path><path d="M691.32288 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z" fill="#EB5480" p-id="60034"></path></svg> diff --git a/src/assets/icons/captcha.svg b/src/assets/icons/captcha.svg new file mode 100644 index 0000000..8b1da30 --- /dev/null +++ b/src/assets/icons/captcha.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M917.6 267.2c-36.1-2.5-72.4-9.3-103.6-19.3-10.1-3-20.2-6.4-30.3-10-21.4-6.3-50.5-18.8-83.6-36.6-.4-.2-.7-.4-1.1-.6-7.8-4.2-15.7-8.7-23.8-13.4-10.9-6.3-21.7-12.9-32.5-19.9-.4-.3-.8-.5-1.2-.8-7.7-5-15.5-10.2-23.1-15.5-5-3.4-10-7.1-15-10.7-3.8-2.8-7.5-5.3-11.3-8.2-27.4-20.5-54.5-43.5-79.9-68.3-25.4 24.8-52.5 47.8-79.9 68.3-3.7 2.8-7.5 5.4-11.3 8.2-5 3.6-10 7.3-15 10.7-7.7 5.4-15.4 10.5-23.1 15.5-.4.3-.8.5-1.2.8-10.8 6.9-21.6 13.6-32.5 19.9-8.1 4.7-16 9.2-23.8 13.4-.3.2-.7.4-1 .6-33 17.8-62.2 30.3-83.6 36.6-10.1 3.6-20.2 7-30.3 10-31.1 10-67.4 16.8-103.6 19.3h.1c1.1 16.2 2.1 37.7 3.4 60.9h.7c6.1 86.8 23.5 210.2 49.7 282.8 1.2 3.2 2.2 6.5 3.3 9.6.6 1.5 1.2 2.8 1.8 4.3 62.8 162.1 171.9 280.1 303 323.4v.4c17.3 5.7 31.9 9.3 43.5 11.5 11.5-2.2 26.1-5.8 43.5-11.5v-.4C687 905 796.1 787 858.9 624.8c.6-1.5 1.2-2.8 1.8-4.3 1.2-3.1 2.2-6.4 3.3-9.6 26.2-72.5 43.6-196 49.7-282.8h.7c1.1-23.3 2.2-44.7 3.2-60.9zm-47.4 41.9-.5 9.5c-.5 2.2-.9 4.4-1 6.6C863 406 847 525.7 821.3 596.7c-.7 1.9-1.4 3.9-2 5.8-.4 1.2-.8 2.5-1.4 4.1-.5 1.2-1 2.5-1.4 3.4C758.1 760.8 657.7 869.3 541 907.8c-1.9.6-3.7 1.4-5.5 2.2-7.9 2.5-15.7 4.6-23.2 6.3-7.5-1.7-15.2-3.8-23.1-6.3-1.8-.9-3.6-1.6-5.5-2.2-116.7-38.5-217.1-147-275.4-297.5-.5-1.2-.9-2.4-1.7-4.1-.4-1.2-.8-2.4-1.3-3.6-.7-2-1.3-3.9-1.9-5.6-25.8-71.2-41.7-191-47.4-271.7-.2-2.3-.5-4.5-1-6.6l-.5-9.3c-.1-1.5-.2-3-.2-4.5 24.6-3.8 48.4-9.3 70-16.2 10.1-3 20.4-6.4 31.4-10.4 25.2-7.6 56.5-21.2 90.5-39.6.6-.3 1.2-.6 1.7-.9 8.2-4.4 16.7-9.2 24.8-14 10.7-6.1 22-13 34.5-21.1.4-.2 1-.6 1.3-.8 8.2-5.3 16.4-10.8 24.1-16.2 4.5-3.1 9.1-6.4 13.7-9.7l2.4-1.8 4-2.9c2.6-1.9 5.2-3.7 7.5-5.5 17.9-13.4 35.3-27.5 52-42.1 16.7 14.7 34 28.7 51.8 42 2.6 1.9 5.1 3.8 7.7 5.6l4.3 3.1 1.5 1.1c4.8 3.5 9.6 6.9 14 9.9 8.1 5.7 16.3 11.2 23.7 16l2.1 1.3c12.4 8 23.7 14.9 34.1 20.8 8.6 5 17 9.8 25 14.1.4.2 1 .5 1.5.8 34.2 18.4 65.6 32.1 90.9 39.7 11 3.9 21.3 7.3 30.6 10.1 22.1 7.1 46.1 12.6 70.8 16.5.1 1.5.1 3 0 4.4z"/><path d="M710.6 411.2 476.1 651.6l-120-123c-8.3-8.5-21.8-8.5-30.1 0s-8.3 22.3 0 30.9L461.1 698c4.2 4.3 9.6 6.4 15.1 6.4 5.4 0 10.9-2.1 15-6.4l249.5-255.7c8.3-8.5 8.3-22.3 0-30.9-8.3-8.7-21.8-8.7-30.1-.2z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/cascader.svg b/src/assets/icons/cascader.svg new file mode 100644 index 0000000..57209bf --- /dev/null +++ b/src/assets/icons/cascader.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M832.128 768c33.195 0 60.501 25.173 63.573 57.813L896 832a64 64 0 0 1-63.872 64H533.205a63.787 63.787 0 0 1-63.872-64 64 64 0 0 1 63.872-64h298.923zM213.333 874.667c-23.722 0-42.666-19.072-42.666-42.624V362.667A42.667 42.667 0 0 1 213.333 320l4.992.299C239.66 322.73 256 340.779 256 362.624l-.043 128.043h128.299c21.248 0 39.595 16.469 42.112 37.674l.299 4.992-.299 4.992A42.368 42.368 0 0 1 384.256 576H256l.043 213.333h128.256c22.869 0 42.41 19.115 42.41 42.667l-.298 4.992a42.368 42.368 0 0 1-42.112 37.675zm618.795-405.334c33.195 0 60.501 25.174 63.573 57.814l.299 6.186a64 64 0 0 1-63.872 64H533.205a63.787 63.787 0 0 1-63.872-64 64 64 0 0 1 63.872-64h298.923zM576.171 128c33.194 0 60.458 25.173 63.573 57.813L640 192c0 35.328-29.013 64-63.83 64H191.83A63.744 63.744 0 0 1 128 192c0-35.328 29.013-64 63.83-64h384.34z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/client.svg b/src/assets/icons/client.svg new file mode 100644 index 0000000..7373b3d --- /dev/null +++ b/src/assets/icons/client.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M962.184 55.874H61.818C27.732 55.874 0 83.606 0 117.692v621.64c0 34.086 27.732 61.818 61.818 61.818h308.52v44.98c0 41.234-33.547 74.782-74.781 74.782h-67.995c-13.036 0-23.606 10.568-23.606 23.606 0 13.038 10.57 23.606 23.606 23.606h568.874c13.036 0 23.606-10.568 23.606-23.606 0-13.038-10.57-23.606-23.606-23.606h-67.997c-41.234 0-74.782-33.548-74.782-74.782v-44.978h308.52c34.087 0 61.821-27.732 61.821-61.819v-621.64c.004-34.087-27.728-61.819-61.814-61.819zM391.84 920.916c16.092-20.672 25.714-46.616 25.714-74.782v-44.98h188.894v44.98c0 28.166 9.622 54.112 25.714 74.782H391.841zm584.95-181.583c0 8.054-6.552 14.608-14.608 14.608H61.818c-8.054 0-14.608-6.552-14.608-14.608V615.267h929.58v124.066zm0-171.28H47.212v-450.36c0-8.055 6.552-14.609 14.608-14.609h900.362c8.054 0 14.61 6.552 14.61 14.608v450.361z"/><path d="M486.531 684.611a25.476 25.476 0 1 0 50.952 0 25.476 25.476 0 1 0-50.952 0zm65.946-466.103c-9.22-9.218-24.162-9.218-33.386 0L352.263 385.337c-9.218 9.218-9.218 24.166 0 33.386a23.534 23.534 0 0 0 16.694 6.914 23.526 23.526 0 0 0 16.692-6.914l166.828-166.829c9.218-9.218 9.218-24.166 0-33.386zm98.88 96.679c-9.216-9.218-24.158-9.218-33.384-.002l-66.46 66.456c-9.218 9.22-9.218 24.168 0 33.386a23.53 23.53 0 0 0 16.692 6.914c6.04 0 12.082-2.304 16.692-6.914l66.46-66.456c9.218-9.218 9.218-24.166 0-33.384z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..e99c978 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 36 36"><path d="m19.41 18 8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29 8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"/></svg> \ No newline at end of file diff --git a/src/assets/icons/close_all.svg b/src/assets/icons/close_all.svg new file mode 100644 index 0000000..2005198 --- /dev/null +++ b/src/assets/icons/close_all.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"/></svg> \ No newline at end of file diff --git a/src/assets/icons/close_left.svg b/src/assets/icons/close_left.svg new file mode 100644 index 0000000..fc5cf71 --- /dev/null +++ b/src/assets/icons/close_left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m7 12 7 7m-7-7 7-7" stroke-linejoin="round"/><path d="M21 12H7.5"/><path d="M3 3v18" stroke-linejoin="round"/></g></svg> \ No newline at end of file diff --git a/src/assets/icons/close_other.svg b/src/assets/icons/close_other.svg new file mode 100644 index 0000000..27ffc32 --- /dev/null +++ b/src/assets/icons/close_other.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"/></svg> \ No newline at end of file diff --git a/src/assets/icons/close_right.svg b/src/assets/icons/close_right.svg new file mode 100644 index 0000000..b96dc1c --- /dev/null +++ b/src/assets/icons/close_right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m17 12-7 7m7-7-7-7" stroke-linejoin="round"/><path d="M3 12h13.5"/><path d="M21 3v18" stroke-linejoin="round"/></g></svg> \ No newline at end of file diff --git a/src/assets/icons/cnblogs.svg b/src/assets/icons/cnblogs.svg new file mode 100644 index 0000000..4920a4c --- /dev/null +++ b/src/assets/icons/cnblogs.svg @@ -0,0 +1 @@ +<svg t="1733555747788" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10924" width="128" height="128"><path d="M851.404 172.596c-187.462-187.461-491.346-187.461-678.808 0-187.461 187.462-187.461 491.346 0 678.808 187.462 187.461 491.346 187.461 678.808 0 187.461-187.462 187.461-491.346 0-678.808zM387.33 728.087a47.084 47.084 0 1 1-66.633-66.502 47.084 47.084 0 0 1 66.633 66.502z m205.527 1.397a38.75 38.75 0 0 1-76.625-11.52h-0.044a6.545 6.545 0 0 0-0.044 0.305v-0.349c0.306-2.618 2.051-20.727-2.967-44.99a174.24 174.24 0 0 0-48.567-89.28 172.102 172.102 0 0 0-88.8-48.305 156.698 156.698 0 0 0-42.458-2.923 38.662 38.662 0 0 1-35.39-65.324 38.618 38.618 0 0 1 21.12-10.822v-0.218c4.452-0.742 111.142-16.45 200.335 72.742 89.018 89.018 74.182 196.145 73.44 200.727z m175.2 7.592a38.75 38.75 0 0 1-65.673 21.382 39.49 39.49 0 0 1-11.65-33.73c0.087-0.35 5.105-37.484-5.062-88.975-13.31-67.375-45.295-126.895-94.953-176.902-50.007-49.702-109.527-81.644-176.945-94.953-51.491-10.167-88.582-5.193-89.019-5.149h0.219-0.044a39.927 39.927 0 0 1-44.684-32.902 38.836 38.836 0 0 1 32.204-44.378c1.92-0.305 47.869-7.33 111.273 4.364a411.753 411.753 0 0 1 106.254 34.952 425.76 425.76 0 0 1 114.633 82.255l0.916 0.96 0.96 0.873a425.89 425.89 0 0 1 82.255 114.72c16.407 33.6 28.145 69.294 34.996 106.21 11.651 63.404 4.67 109.353 4.32 111.273z" fill="#1296DB" p-id="10925"></path></svg> diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg new file mode 100644 index 0000000..d8b546c --- /dev/null +++ b/src/assets/icons/code.svg @@ -0,0 +1 @@ +<svg t="1720831003829" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5159" width="200" height="200"><path d="M438.4 849.1l222.7-646.7c0.2-0.5 0.3-1.1 0.4-1.6L438.4 849.1z" opacity=".224" p-id="5160"></path><path d="M661.2 168.7h-67.5c-3.4 0-6.5 2.2-7.6 5.4L354.7 846c-0.3 0.8-0.4 1.7-0.4 2.6 0 4.4 3.6 8 8 8h67.8c3.4 0 6.5-2.2 7.6-5.4l0.7-2.1 223.1-648.3 7.4-21.4c0.3-0.8 0.4-1.7 0.4-2.6-0.1-4.5-3.6-8.1-8.1-8.1zM954.6 502.1c-0.8-1-1.7-1.9-2.7-2.7l-219-171.3c-3.5-2.7-8.5-2.1-11.2 1.4-1.1 1.4-1.7 3.1-1.7 4.9v81.3c0 2.5 1.1 4.8 3.1 6.3l115 90-115 90c-1.9 1.5-3.1 3.8-3.1 6.3v81.3c0 4.4 3.6 8 8 8 1.8 0 3.5-0.6 4.9-1.7l219-171.3c6.9-5.4 8.2-15.5 2.7-22.5zM291.1 328.1l-219 171.3c-1 0.8-1.9 1.7-2.7 2.7-5.4 7-4.2 17 2.7 22.5l219 171.3c1.4 1.1 3.1 1.7 4.9 1.7 4.4 0 8-3.6 8-8v-81.3c0-2.5-1.1-4.8-3.1-6.3l-115-90 115-90c1.9-1.5 3.1-3.8 3.1-6.3v-81.3c0-1.8-0.6-3.5-1.7-4.9-2.7-3.5-7.7-4.1-11.2-1.4z" p-id="5161"></path></svg> diff --git a/src/assets/icons/collapse.svg b/src/assets/icons/collapse.svg new file mode 100644 index 0000000..1507568 --- /dev/null +++ b/src/assets/icons/collapse.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4h18v2H3V4zm0 15h18v2H3v-2zm8-5h10v2H11v-2zm0-5h10v2H11V9zm-8 3.5L7 9v7l-4-3.5z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/csdn.svg b/src/assets/icons/csdn.svg new file mode 100644 index 0000000..e16bad0 --- /dev/null +++ b/src/assets/icons/csdn.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30px" height="30px" viewBox="0 0 30 30" version="1.1"> + <title>ic/csdn</title> + <g id="ic/csdn" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <path d="M24.7385612,21.818791 C24.4728206,21.5662432 24.1090763,21.4267765 23.7575825,21.4343152 C23.3966653,21.4399693 23.0696724,21.5907441 22.8378562,21.8555424 C20.7581061,24.2349574 17.1347988,24.4922169 15.6732254,24.4922169 C12.9611634,24.4922169 10.886125,23.8043069 9.50559315,22.4501605 C8.19385225,21.1638629 7.50594216,19.2678696 7.46070972,16.8168365 C7.35516735,11.1345107 10.5732673,5.25806226 16.139685,5.25806226 C18.7980335,5.25806226 20.8627061,7.14368979 21.6260036,7.95410443 C21.8917442,8.23586486 22.2583155,8.39794779 22.6352525,8.39983247 C23.0131319,8.4092559 23.362741,8.24151892 23.599269,7.96164317 L23.8160078,7.70532598 C24.2607935,7.18232584 24.4605701,6.50572386 24.380471,5.80179394 C24.2984872,5.09126763 23.9422817,4.44293592 23.3778185,3.97459165 C22.0133064,2.84472288 19.6951436,1.5 16.3969445,1.5 C12.9715292,1.5 9.58757695,3.07465447 7.1129853,5.82441016 C4.51306208,8.71269021 3.1240491,12.6441435 3.20320588,16.895051 C3.26634283,20.3063311 4.38490349,23.1729373 6.44015269,25.1886081 C8.64806138,27.3550537 11.8821812,28.5 15.7947876,28.5 C20.3849384,28.5 23.2289283,27.1401996 24.8082945,26.0009074 C25.4198749,25.5608334 25.7845615,24.8644423 25.8128317,24.093606 C25.8392173,23.3190004 25.5225902,22.5604146 24.9430495,22.0119712 L24.7385612,21.818791 Z" id="Fill-1" fill="#FC5533"/> + </g> +<script xmlns=""/></svg> diff --git a/src/assets/icons/dict.svg b/src/assets/icons/dict.svg new file mode 100644 index 0000000..db60220 --- /dev/null +++ b/src/assets/icons/dict.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M449.6 116.2H303.8c-14.2 0-25.7-11.5-25.7-25.7s11.5-25.7 25.7-25.7h145.8c14.2 0 25.7 11.5 25.7 25.7s-11.5 25.7-25.7 25.7zm0 0"/><path d="M160.1 859.3c-14.2 0-25.7-11.5-25.7-25.7V167.4c0-56.6 46-102.6 102.6-102.6h66.8c14.2 0 25.7 11.5 25.7 25.7s-11.5 25.7-25.7 25.7H237c-28.2 0-51.1 22.9-51.1 51.1v666.2c-.1 14.3-11.6 25.8-25.8 25.8zm373.5-512.6c-6.3 0-12.4-1.3-17.6-3.5-13.5-5.8-21.9-17.9-21.9-31.6v-221c0-14.2 11.5-25.7 25.7-25.7s25.7 11.5 25.7 25.7v189l27.7-26.6c14.1-13.5 36.1-13.5 50.1 0l22.1 21.3V90.5c0-14.2 11.5-25.7 25.7-25.7s25.7 11.5 25.7 25.7v219.6c0 14.5-8.6 27.5-22 33.2-13.3 5.7-28.7 2.9-39.2-7.2l-37.5-36-37.5 36c-7.6 7.6-17.5 10.6-27 10.6zm0 0"/><path d="M846.1 958.9H236.9c-56.6 0-102.6-46-102.6-102.6v-22.8c0-14.2 11.5-25.7 25.7-25.7s25.7 11.5 25.7 25.7v22.8c0 28.2 22.9 51.1 51.1 51.1H846c14.2 0 25.7 11.5 25.7 25.7.1 14.3-11.4 25.8-25.6 25.8zm0 0"/><path d="M160.1 876h-.9c-14.2-.5-25.3-12.4-24.8-26.6 1-28.2 6.3-48.5 16.7-63.6 13.8-20.1 35.4-30.3 64.3-30.3h615c3.2-2.7 6.4-6.1 8.6-8.6V133.1c-1.8-5.1-11.7-15-16.8-16.8H449.6c-14.2 0-25.7-11.5-25.7-25.7s11.5-25.7 25.7-25.7h373.6c19.8 0 36.7 13.9 45 22.2 8.3 8.3 22.2 25.2 22.2 45v621.6c0 10.8-6.2 19.6-12.3 26.7-4.6 5.4-10.3 11-15.6 15.4-1 .9-2.1 1.7-3.2 2.5-5.4 4.1-12.9 8.8-22.3 8.8H215.3c-15 0-28 0-29.5 44.2-.5 13.8-11.9 24.7-25.7 24.7zm0 0"/><path d="M284.4 806.4c-14.2 0-25.7-11.5-25.7-25.7V90.5c0-14.2 11.5-25.7 25.7-25.7s25.7 11.5 25.7 25.7v690.1c0 14.3-11.5 25.8-25.7 25.8zM844.9 959h-1.6c-6.6-.3-30-2.3-52.2-16.9-19.5-12.7-42.6-38-42.6-86.3 0-62.3 35.7-101 93.1-101 14.2 0 25.7 11.5 25.7 25.7s-11.5 25.7-25.7 25.7c-12.5 0-41.7 0-41.7 49.6 0 21 6.6 35.3 20.1 43.8 10.6 6.6 22.1 7.8 25 8 1.4-.1 2.9 0 4.4.2 13.7 1.7 23.6 14 22.5 27.7-.9 9.5-8.8 23.5-27 23.5zm-1.8-51.3c-1.1.1-2.3.3-3.4.6 1.1-.3 2.2-.5 3.4-.6zm0 0"/></svg> \ No newline at end of file diff --git a/src/assets/icons/document.svg b/src/assets/icons/document.svg new file mode 100644 index 0000000..aaa0574 --- /dev/null +++ b/src/assets/icons/document.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M832.1 185.1H609.4l-17.1-62c-9.6-34.6-40.5-58.8-75.3-58.8H196c-43.2 0-78.3 36.4-78.3 81.1V897c0 35.3 28.7 64 64 64H832c35.3 0 64-28.7 64-64V249c.1-35.2-28.6-63.9-63.9-63.9zm-644.4-39.7c0-6.6 4.4-11.1 8.3-11.1h321c3.4 0 6.6 3.1 7.8 7.4l12 43.4H187.7v-39.7zm638.4 745.8H187.7V255.1h638.4v636.1z"/><path d="M311.1 415.1a35 35 0 1 0 70 0 35 35 0 1 0-70 0zm151.2-35h257.8v70H462.3zM311.1 582.3a35 35 0 1 0 70 0 35 35 0 1 0-70 0zm151.2-35h257.8v70H462.3zM311.1 749.5a35 35 0 1 0 70 0 35 35 0 1 0-70 0zm151.2-35h257.8v70H462.3z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/down.svg b/src/assets/icons/down.svg new file mode 100644 index 0000000..5fc8b88 --- /dev/null +++ b/src/assets/icons/down.svg @@ -0,0 +1 @@ +<svg width="15" height="15" aria-label="鍚戜笅閿�" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3"></path></g></svg> diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..a8077dc --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M624 706.3h-74.1V464c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v242.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.7c3.2 4.1 9.4 4.1 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9z"/><path d="M811.4 366.7C765.6 245.9 648.9 160 512.2 160S258.8 245.8 213 366.6C127.3 389.1 64 467.2 64 560c0 110.5 89.5 200 199.9 200H304c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8h-40.1c-33.7 0-65.4-13.4-89-37.7-23.5-24.2-36-56.8-34.9-90.6.9-26.4 9.9-51.2 26.2-72.1 16.7-21.3 40.1-36.8 66.1-43.7l37.9-9.9 13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4 14.9-19.2 32.6-35.9 52.4-49.9 41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9 15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5 37.8 10C846.1 454.5 884 503.8 884 560c0 33.1-12.9 64.3-36.3 87.7-23.4 23.4-54.5 36.3-87.6 36.3H720c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h40.1C870.5 760 960 670.5 960 560c0-92.7-63.1-170.7-148.6-193.3z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/file.svg b/src/assets/icons/file.svg new file mode 100644 index 0000000..fac9bf0 --- /dev/null +++ b/src/assets/icons/file.svg @@ -0,0 +1 @@ +<svg t="1721541550402" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1592" width="200" height="200"><path d="M979.096493 980.950486H44.904061C19.829898 980.950486 0.000277 960.442811 0.000277 935.839135v-740.047567c0-25.184865 20.410811-45.111351 44.903784-45.111352h934.192432c25.074162 0 44.903784 20.507676 44.903784 45.111352v740.047567c0 25.184865-20.410811 45.111351-44.903784 45.111351z" fill="#FFA000" p-id="1593"></path><path d="M512.000277 344.409946H0.000277V112.902919a45.097514 45.097514 0 0 1 44.903784-45.24973h350.470918c19.829622 0 37.320649 12.924541 43.146379 32.311352L512.000277 344.423784z" fill="#FFA000" p-id="1594"></path><path d="M909.699736 925.599135H114.300817c-25.184865 0-45.111351-20.134054-45.111351-44.281081v-603.32973c0-24.728216 20.493838-44.281081 45.111351-44.281081h795.398919c25.184865 0 45.111351 20.134054 45.111352 44.281081v603.32973c0.567351 24.147027-19.926486 44.281081-45.111352 44.281081z" fill="#FFFFFF" p-id="1595"></path><path d="M979.096493 980.950486H44.904061C19.829898 980.950486 0.000277 960.802595 0.000277 936.627892V361.056865c0-24.755892 20.410811-44.322595 44.903784-44.322595h934.192432c25.074162 0 44.903784 20.147892 44.903784 44.322595v575.571027c0 24.755892-20.410811 44.322595-44.903784 44.322594z" fill="#FFCA28" p-id="1596"></path><path d="M364.46125 485.708108H106.634655C93.917682 485.708108 83.027304 476.021622 83.027304 463.512216c0-11.96973 10.295351-22.223568 23.607351-22.223567h257.21773c12.716973 0 23.607351 9.686486 23.607351 22.223567 0 11.955892-10.295351 22.223568-22.998486 22.223568z m0 149.296433H106.634655c-12.716973 0-23.607351-9.686486-23.607351-22.223568 0-12.537081 10.295351-22.223568 23.607351-22.223568h257.21773c12.716973 0 23.607351 9.686486 23.607351 22.223568 0 12.537081-10.295351 22.223568-22.998486 22.223568z" fill="#FFF8E1" p-id="1597"></path></svg> diff --git a/src/assets/icons/fullscreen-exit.svg b/src/assets/icons/fullscreen-exit.svg new file mode 100644 index 0000000..2452f2b --- /dev/null +++ b/src/assets/icons/fullscreen-exit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/fullscreen.svg b/src/assets/icons/fullscreen.svg new file mode 100644 index 0000000..4b6ee11 --- /dev/null +++ b/src/assets/icons/fullscreen.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 3v2H4v4H2V3h6zM2 21v-6h2v4h4v2H2zm20 0h-6v-2h4v-4h2v6zm0-12h-2V5h-4V3h6v6z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/gitcode.svg b/src/assets/icons/gitcode.svg new file mode 100644 index 0000000..7a02760 --- /dev/null +++ b/src/assets/icons/gitcode.svg @@ -0,0 +1 @@ +<svg width="24" height="24" class="icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.4791 4.97677C15.6201 4.89789 15.7691 4.8145 15.93 4.72314C15.9494 4.82848 15.9683 4.92046 15.9854 5.00383C16.0157 5.15091 16.0406 5.2719 16.0525 5.39144C16.1479 6.4296 16.6697 7.1933 17.4092 7.36527C18.4908 7.61639 19.5106 7.20133 20.06 6.28555C20.72 5.18673 20.4334 3.84099 19.3097 3.03098C16.1851 0.777435 12.7523 0.155888 9.05448 1.24127C1.08137 3.59371 -1.64675 13.3884 4.01196 19.3949C6.43291 21.9642 9.50695 23.0727 12.9963 22.9889C17.4663 22.8839 20.6857 20.6563 22.7408 16.7954C24.1978 14.0561 22.6139 11.0619 19.5805 10.4396C17.8481 10.0908 16.0765 9.97756 14.3137 10.103C13.7272 10.1594 13.1579 10.3325 12.6394 10.6124C12.0592 10.9135 11.8915 11.5383 11.9565 12.1575C12.0171 12.7217 12.4498 13.0601 12.965 13.1453C14.0024 13.3077 15.0522 13.402 16.1 13.4881C16.4032 13.5136 16.7093 13.5166 17.0149 13.5197C17.4534 13.5241 17.8912 13.5285 18.3187 13.5991C19.5385 13.8007 19.9574 14.7905 19.33 15.8495C19.1763 16.1041 18.9971 16.3424 18.7951 16.5607C17.9745 17.4632 16.9014 18.0981 15.7152 18.3827C13.55 18.9127 11.3827 18.9425 9.22755 18.2617C6.77347 17.4875 5.31042 15.6849 5.25902 13.2584C5.2398 11.7619 5.61972 10.2874 6.35969 8.9865C6.694 8.38013 6.87751 7.75562 6.82593 7.06851C6.80422 6.77557 6.79219 6.48231 6.77927 6.16716C6.77239 5.99944 6.76526 5.82551 6.75628 5.64214C7.00484 5.69431 7.25032 5.76016 7.49161 5.83943C8.43027 6.21622 9.35415 6.38811 10.3702 6.11155C10.9481 5.97335 11.5455 5.93511 12.1363 5.9985C13.0877 6.07606 14.0387 5.84361 14.847 5.33586C15.0488 5.2176 15.2539 5.10279 15.4791 4.97677Z" fill="currentColor"></path></svg> diff --git a/src/assets/icons/gitee.svg b/src/assets/icons/gitee.svg new file mode 100644 index 0000000..c799c2f --- /dev/null +++ b/src/assets/icons/gitee.svg @@ -0,0 +1 @@ +<svg t="1725812178308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4564" width="200" height="200"><path d="M512 1024C229.234 1024 0 794.766 0 512S229.234 0 512 0s512 229.234 512 512-229.234 512-512 512z m259.157-568.889l-290.759 0.014c-13.966 0-25.287 11.321-25.287 25.273l-0.028 63.218c0 13.966 11.306 25.287 25.273 25.287H657.38c13.966 0 25.287 11.307 25.287 25.273v12.644a75.847 75.847 0 0 1-75.847 75.847H366.606a25.287 25.287 0 0 1-25.287-25.273v-240.2a75.847 75.847 0 0 1 75.847-75.846l353.92-0.015c13.966 0 25.273-11.306 25.287-25.273l0.071-63.189c0-13.966-11.306-25.287-25.272-25.301l-353.992 0.014c-104.718-0.014-189.624 84.892-189.624 189.61v353.963c0 13.967 11.32 25.287 25.287 25.287h372.935c94.265 0 170.666-76.401 170.666-170.666v-145.38c0-13.952-11.32-25.273-25.287-25.273z" p-id="4565"></path></svg> diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 0000000..1adfa4e --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M511.543 14.057C228.914 13.943 0 242.743 0 525.143 0 748.457 143.2 938.286 342.629 1008c26.857 6.743 22.742-12.343 22.742-25.371v-88.572C210.286 912.23 204 809.6 193.6 792.457c-21.029-35.886-70.743-45.028-55.886-62.171 35.315-18.172 71.315 4.571 113.029 66.171 30.171 44.686 89.028 37.143 118.857 29.714 6.514-26.857 20.457-50.857 39.657-69.485C248.571 727.886 181.6 629.829 181.6 513.257c0-56.571 18.629-108.571 55.2-150.514-23.314-69.143 2.171-128.343 5.6-137.143 66.4-5.943 135.429 47.543 140.8 51.771C420.914 267.2 464 261.83 512.229 261.83c48.457 0 91.657 5.6 129.714 15.885 12.914-9.828 76.914-55.771 138.628-50.171 3.315 8.8 28.229 66.628 6.286 134.857 37.029 42.057 55.886 94.514 55.886 151.2 0 116.8-67.429 214.971-228.572 243.314a145.714 145.714 0 0 1 43.543 104v128.572c.915 10.285 0 20.457 17.143 20.457 202.4-68.229 348.114-259.429 348.114-484.686 0-282.514-229.028-511.2-511.428-511.2z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/homepage.svg b/src/assets/icons/homepage.svg new file mode 100644 index 0000000..1e1feab --- /dev/null +++ b/src/assets/icons/homepage.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M958.401 451.55a20.01 20.01 0 0 0-6.966-14.972L524.345 69.511c-7.499-6.446-18.581-6.446-26.08 0L309.583 231.676V129.657c0-11.05-8.902-19.533-19.952-19.533h-88.034c-11.048 0-19.928 8.482-19.928 19.533v211.954L71.176 436.578a20.003 20.003 0 0 0-6.968 15.174v105.5a20.007 20.007 0 0 0 33.052 15.172l53.298-45.826V850.7c0 60.678 49.364 110.042 110.042 110.042h504.192c60.678 0 110.043-49.364 110.043-110.042V527.026l51.586 44.336a20.001 20.001 0 0 0 21.48 2.966 20.006 20.006 0 0 0 11.566-18.343l-1.066-104.436zM221.579 150.033h48.095v115.942l-48.095 41.336V150.034zm349.14 770.692H436.665V700.642c0-11.03 8.977-20.007 20.008-20.007h94.036c11.03 0 20.007 8.976 20.007 20.007v220.084zm264.1-424.83v354.803c0 38.612-31.415 70.027-70.028 70.027H610.733V700.642c0-33.096-26.927-60.023-60.023-60.023h-94.036c-33.097 0-60.023 26.927-60.023 60.023v220.085H260.599c-38.612 0-70.027-31.415-70.027-70.027V495.895a20.07 20.07 0 0 0-.315-3.432L512.37 215.504l322.703 277.349a20.158 20.158 0 0 0-.255 3.042zM525.41 173.947c-7.502-6.446-18.587-6.447-26.086.003l-395.1 339.714v-52.727l407.081-349.87 407.177 349.952.522 51.205L525.41 173.948z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/juejin.svg b/src/assets/icons/juejin.svg new file mode 100644 index 0000000..937ace3 --- /dev/null +++ b/src/assets/icons/juejin.svg @@ -0,0 +1 @@ +<svg t="1733555774238" class="icon" viewBox="0 0 1316 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46620" width="128" height="128"><path d="M643.181714 247.698286l154.916572-123.172572L643.181714 0.256 643.072 0l-154.660571 124.269714 154.660571 123.245715 0.109714 0.182857z m0 388.461714h0.109715l399.579428-315.245714-108.361143-87.04-291.218285 229.888h-0.146286l-0.109714 0.146285L351.817143 234.093714l-108.251429 87.04 399.433143 315.136 0.146286-0.146285z m-0.146285 215.552l0.146285-0.146286 534.893715-422.034285 108.397714 87.04-243.309714 192L643.145143 1024 10.422857 525.056 0 516.754286l108.251429-86.893715L643.035429 851.748571z" fill="#1E80FF" p-id="46621"></path></svg> diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg new file mode 100644 index 0000000..f5875d3 --- /dev/null +++ b/src/assets/icons/menu.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M374.272 440.832H127.488c-33.792 0-61.44-27.648-61.44-61.44V132.608c0-33.792 27.648-61.44 61.44-61.44h247.296c33.792 0 61.44 27.648 61.44 61.44v247.296c-.512 33.792-27.648 60.928-61.952 60.928zM127.488 132.608v247.296h247.296V132.608H127.488zM762.88 492.032c-16.384 0-31.744-6.144-43.52-17.92L544.768 299.52c-11.776-11.776-17.92-27.136-17.92-43.52s6.144-31.744 17.92-43.52L719.36 37.888c11.776-11.776 27.136-17.92 43.52-17.92s31.744 6.144 43.52 17.92L980.992 212.48c11.776 11.776 17.92 27.136 17.92 43.52s-6.144 31.744-17.92 43.52L806.4 474.112c-11.776 11.776-27.136 17.92-43.52 17.92zm0-410.624L588.288 256 762.88 430.592 937.472 256 762.88 81.408zM374.272 952.832H127.488c-33.792 0-61.44-27.648-61.44-61.44V644.096c0-33.792 27.648-61.44 61.44-61.44h247.296c33.792 0 61.44 27.648 61.44 61.44v247.296c-.512 34.304-27.648 61.44-61.952 61.44zM127.488 644.608v247.296h247.296V644.608H127.488zm758.784 308.224H638.976c-33.792 0-61.44-27.648-61.44-61.44V644.096c0-33.792 27.648-61.44 61.44-61.44h247.296c33.792 0 61.44 27.648 61.44 61.44v247.296c0 34.304-27.136 61.44-61.44 61.44zM639.488 644.608v247.296h247.296V644.608H639.488z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/qq.svg b/src/assets/icons/qq.svg new file mode 100644 index 0000000..a59086b --- /dev/null +++ b/src/assets/icons/qq.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.9139 14.529C19.7336 13.955 19.4877 13.2856 19.2385 12.643L18.3288 10.3969C18.3295 10.371 18.3408 9.92858 18.3408 9.70053C18.3408 5.8599 16.5082 2.00037 12.0009 2C7.49403 2.00037 5.66113 5.8599 5.66113 9.70053C5.66113 9.92858 5.67237 10.371 5.67312 10.3969L4.76379 12.643C4.51453 13.2856 4.26827 13.955 4.08798 14.529C3.2285 17.2657 3.507 18.3982 3.71915 18.4238C4.17419 18.4779 5.49021 16.3635 5.49021 16.3635C5.49021 17.5879 6.12741 19.1858 7.5064 20.3398C6.99064 20.4971 6.35868 20.7388 5.95237 21.0355C5.58729 21.3025 5.63302 21.5743 5.69861 21.6841C5.9876 22.1661 10.6542 21.9918 12.0017 21.8417C13.3488 21.9918 18.0158 22.1661 18.3044 21.6841C18.3704 21.5743 18.4157 21.3025 18.0507 21.0355C17.6443 20.7388 17.012 20.4971 16.4959 20.3395C17.8745 19.1858 18.5117 17.5879 18.5117 16.3635C18.5117 16.3635 19.8281 18.4779 20.2831 18.4238C20.4949 18.3982 20.7734 17.2657 19.9139 14.529Z"></path></svg> diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg new file mode 100644 index 0000000..e598ed1 --- /dev/null +++ b/src/assets/icons/refresh.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 512 512"><path d="m400 148-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"/><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"/></svg> \ No newline at end of file diff --git a/src/assets/icons/role.svg b/src/assets/icons/role.svg new file mode 100644 index 0000000..5d25278 --- /dev/null +++ b/src/assets/icons/role.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="256" height="256"><path d="M79.238 961.896v-25.442c0-109.28 28.835-214.892 81.139-297.416 48.427-76.396 115.304-131.573 195.508-161.896A240.785 240.785 0 0 1 279.488 300.5c0-131.538 104.331-238.535 232.547-238.535S744.546 168.962 744.546 300.5a240.854 240.854 0 0 1-76.742 176.988c190.87 73.004 276.992 277.131 276.992 458.966v25.442H79.238zM694.908 300.5c0-103.43-82.039-187.615-182.873-187.615-100.835 0-182.873 84.184-182.873 187.615 0 103.465 82.038 187.65 182.873 187.65 100.834 0 182.873-84.185 182.873-187.65zm-79.166 213.508a226.454 226.454 0 0 1-103.707 25.096A225.935 225.935 0 0 1 407.912 513.8C212.888 564.927 136.804 752.854 129.5 910.977h765.035c-7.997-167.4-95.227-347.746-278.793-396.97zm-143.411 37.246h79.407l39.739-8.48-45.242 65.664 30.6 227.527-64.8 56.908-69.197-56.908 40.535-227.527-50.78-65.665 39.738 8.48z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/setting.svg b/src/assets/icons/setting.svg new file mode 100644 index 0000000..fbc4945 --- /dev/null +++ b/src/assets/icons/setting.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="m12 1 9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 2.311L4.5 7.653v8.694l7.5 4.342 7.5-4.342V7.653L12 3.311zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/size.svg b/src/assets/icons/size.svg new file mode 100644 index 0000000..f92f852 --- /dev/null +++ b/src/assets/icons/size.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6v15H8V6H2V4h14v2h-6zm8 8v7h-2v-7h-3v-2h8v2h-3z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/system.svg b/src/assets/icons/system.svg new file mode 100644 index 0000000..2e6045b --- /dev/null +++ b/src/assets/icons/system.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M139 669.6V164.3c0-12.7 10.3-23.1 23.1-23.1h694.4c12.7 0 23.1 10.4 23.1 23.1v248.5h70V164.3c0-51.3-41.8-93.1-93.1-93.1H162c-51.3.1-93 41.8-93 93.1v505.3c0 51.3 41.8 93.1 93.1 93.1h224.7v-70H162c-12.7 0-23-10.4-23-23.1zm-34.3 131h282v70h-282z"/><path d="m954.9 599.4-5.1-15c-11.5-33.9-29.4-64.9-53.2-91.9l-10.5-11.9h-83.2l-41.7-72.2-15.6-3.1c-34.8-6.9-71.3-6.9-106.1 0l-15.6 3.1-41.7 72.2H499l-10.5 11.9c-23.8 27.1-41.7 58-53.2 91.9l-5.1 15 41.7 72.2-41.7 72.2 5.1 15c11.5 33.9 29.4 64.9 53.2 91.9l10.5 11.9h83.2l41.7 72.2 15.6 3.1c17.4 3.5 35.3 5.2 53.1 5.2s35.6-1.8 53.1-5.2l15.6-3.1 41.7-72.2h83.2l10.5-11.9c23.8-27.1 41.7-58 53.2-91.9l5.1-15-41.7-72.2 41.6-72.2zm-76.8 151.2c-6.4 14.9-14.5 29-24.3 42h-91.2l-45.6 79c-16.1 1.9-32.4 1.9-48.5 0l-45.6-79h-91.2c-9.8-13-17.9-27-24.3-42l45.6-79.1-45.6-79.1c6.4-14.9 14.5-29 24.3-42h91.2l45.6-79c16.1-1.9 32.4-1.9 48.5 0l45.6 79h91.2c9.8 13 17.9 27 24.3 42l-45.6 79.1 45.6 79.1z"/><path d="M692.7 560.2c-61.4 0-111.3 49.9-111.3 111.3s49.9 111.3 111.3 111.3S804 732.9 804 671.5c0-61.3-49.9-111.3-111.3-111.3zm0 152.7c-22.8 0-41.3-18.5-41.3-41.3s18.5-41.3 41.3-41.3 41.3 18.5 41.3 41.3-18.5 41.3-41.3 41.3z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/tree.svg b/src/assets/icons/tree.svg new file mode 100644 index 0000000..51aea8f --- /dev/null +++ b/src/assets/icons/tree.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M977.455 558.545h-34.91V453.818c0-44.218-37.236-81.454-81.454-81.454H546.909v-93.091h197.818c25.6 0 46.546-20.946 46.546-46.546V93.091c0-25.6-20.946-46.546-46.546-46.546H279.273c-25.6 0-46.546 20.946-46.546 46.546v139.636c0 25.6 20.946 46.546 46.546 46.546H477.09v93.09H162.909c-44.218 0-81.454 37.237-81.454 81.455v104.727h-34.91C20.945 558.545 0 579.491 0 605.091v325.818c0 25.6 20.945 46.546 46.545 46.546h139.637c25.6 0 46.545-20.946 46.545-46.546V605.091c0-25.6-20.945-46.546-46.545-46.546h-34.91V453.818c0-6.982 4.655-11.636 11.637-11.636h314.182v116.363h-34.91c-25.6 0-46.545 20.946-46.545 46.546v325.818c0 25.6 20.946 46.546 46.546 46.546h139.636c25.6 0 46.546-20.946 46.546-46.546V605.091c0-25.6-20.946-46.546-46.546-46.546H546.91V442.182h314.182c6.982 0 11.636 4.654 11.636 11.636v104.727h-34.909c-25.6 0-46.545 20.946-46.545 46.546v325.818c0 25.6 20.945 46.546 46.545 46.546h139.637c25.6 0 46.545-20.946 46.545-46.546V605.091c0-25.6-20.945-46.546-46.545-46.546zm-814.546 69.819v279.272H69.82V628.364h93.09zm395.636 0v279.272h-93.09V628.364h93.09zm-256-418.91v-93.09h418.91v93.09h-418.91zm651.637 698.182H861.09V628.364h93.09v279.272z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/up.svg b/src/assets/icons/up.svg new file mode 100644 index 0000000..3b6c535 --- /dev/null +++ b/src/assets/icons/up.svg @@ -0,0 +1 @@ +<svg width="15" height="15" aria-label="鍚戜笂閿�" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3"></path></g></svg> diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg new file mode 100644 index 0000000..8e693ec --- /dev/null +++ b/src/assets/icons/user.svg @@ -0,0 +1 @@ +<svg t="1719843414729" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="175406" width="200" height="200"><path d="M512 1024C229.236364 1024 0 794.763636 0 512S229.236364 0 512 0s512 229.236364 512 512-229.236364 512-512 512z m56.599273-542.72a153.134545 153.134545 0 0 0 97.652363-142.149818C666.251636 254.789818 597.038545 186.181818 512 186.181818S357.794909 254.789818 357.794909 339.130182a153.134545 153.134545 0 0 0 97.605818 142.149818C328.471273 507.345455 232.727273 619.054545 232.727273 752.500364c0 32.488727 22.993455 43.240727 41.425454 51.851636l1.070546 0.465455c74.333091 26.065455 181.992727 33.000727 233.797818 33.000727 56.552727 0 167.191273-9.122909 240.686545-34.304 27.601455-10.472727 41.565091-27.648 41.565091-51.013818 0-133.492364-95.744-245.201455-222.673454-271.220364z" fill="#17E3C7" p-id="175407"></path></svg> diff --git a/src/assets/icons/wechat.svg b/src/assets/icons/wechat.svg new file mode 100644 index 0000000..2fc5803 --- /dev/null +++ b/src/assets/icons/wechat.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.5753 13.7114C19.0742 13.7114 19.4733 13.2873 19.4733 12.8134C19.4733 12.3145 19.0742 11.9155 18.5753 11.9155C18.0765 11.9155 17.6774 12.3145 17.6774 12.8134C17.6774 13.3123 18.0765 13.7114 18.5753 13.7114ZM14.1497 13.7114C14.6485 13.7114 15.0476 13.2873 15.0476 12.8134C15.0476 12.3145 14.6485 11.9155 14.1497 11.9155C13.6508 11.9155 13.2517 12.3145 13.2517 12.8134C13.2517 13.3123 13.6508 13.7114 14.1497 13.7114ZM20.717 18.7516C20.5942 18.8253 20.5205 18.9482 20.5451 19.1202C20.5451 19.1693 20.5451 19.2185 20.5696 19.2676C20.6679 19.6854 20.8643 20.349 20.8643 20.3736C20.8643 20.4473 20.8889 20.4964 20.8889 20.5456C20.8889 20.6685 20.7907 20.7668 20.6679 20.7668C20.6187 20.7668 20.5942 20.7422 20.5451 20.7176L19.0961 19.882C18.9978 19.8329 18.875 19.7837 18.7522 19.7837C18.6786 19.7837 18.6049 19.7837 18.5558 19.8083C17.8681 20.0049 17.1559 20.1032 16.3946 20.1032C12.7352 20.1032 9.78815 17.6456 9.78815 14.5983C9.78815 11.5509 12.7352 9.09329 16.3946 9.09329C20.0539 9.09329 23.001 11.5509 23.001 14.5983C23.001 16.2448 22.1168 17.7439 20.717 18.7516ZM16.6737 8.09757C16.581 8.09473 16.488 8.09329 16.3946 8.09329C12.2199 8.09329 8.78815 10.9536 8.78815 14.5983C8.78815 15.1519 8.86733 15.6874 9.01626 16.1975H8.92711C8.04096 16.1975 7.15481 16.0503 6.3425 15.8296C6.26866 15.805 6.19481 15.805 6.12097 15.805C5.97327 15.805 5.82558 15.8541 5.7025 15.9277L3.95482 16.9334C3.90559 16.958 3.85635 16.9825 3.80712 16.9825C3.65943 16.9825 3.53636 16.8599 3.53636 16.7127C3.53636 16.6391 3.56097 16.59 3.58559 16.5164C3.6102 16.4919 3.83174 15.6824 3.95482 15.1918C3.95482 15.1427 3.97943 15.0691 3.97943 15.0201C3.97943 14.8238 3.88097 14.6766 3.75789 14.5785C2.05944 13.3765 1.00098 11.5858 1.00098 9.59876C1.00098 5.94369 4.5702 3 8.95173 3C12.7157 3 15.8802 5.16856 16.6737 8.09757ZM11.5199 8.51604C12.0927 8.51604 12.5462 8.03871 12.5462 7.4898C12.5462 6.91701 12.0927 6.46356 11.5199 6.46356C10.9471 6.46356 10.4937 6.91701 10.4937 7.4898C10.4937 8.06258 10.9471 8.51604 11.5199 8.51604ZM6.26045 8.51604C6.83324 8.51604 7.28669 8.03871 7.28669 7.4898C7.28669 6.91701 6.83324 6.46356 6.26045 6.46356C5.68767 6.46356 5.23421 6.91701 5.23421 7.4898C5.23421 8.06258 5.68767 8.51604 6.26045 8.51604Z"></path></svg> diff --git a/src/assets/images/401.gif b/src/assets/images/401.gif new file mode 100644 index 0000000..4c930e7 --- /dev/null +++ b/src/assets/images/401.gif Binary files differ diff --git a/src/assets/images/404.png b/src/assets/images/404.png new file mode 100644 index 0000000..4e09829 --- /dev/null +++ b/src/assets/images/404.png Binary files differ diff --git a/src/assets/images/404_cloud.png b/src/assets/images/404_cloud.png new file mode 100644 index 0000000..a81d8da --- /dev/null +++ b/src/assets/images/404_cloud.png Binary files differ diff --git a/src/assets/images/login-bg-dark.jpg b/src/assets/images/login-bg-dark.jpg new file mode 100644 index 0000000..50dc817 --- /dev/null +++ b/src/assets/images/login-bg-dark.jpg Binary files differ diff --git a/src/assets/images/login-bg.jpg b/src/assets/images/login-bg.jpg new file mode 100644 index 0000000..993b958 --- /dev/null +++ b/src/assets/images/login-bg.jpg Binary files differ diff --git a/src/assets/images/user-logo.gif b/src/assets/images/user-logo.gif new file mode 100644 index 0000000..b4dd41b --- /dev/null +++ b/src/assets/images/user-logo.gif Binary files differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..524fb13 --- /dev/null +++ b/src/assets/logo.png Binary files differ diff --git a/src/components/AppLink/index.vue b/src/components/AppLink/index.vue new file mode 100644 index 0000000..b97aacf --- /dev/null +++ b/src/components/AppLink/index.vue @@ -0,0 +1,38 @@ +<template> + <component :is="linkType" v-bind="linkProps(to)"> + <slot /> + </component> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "AppLink", + inheritAttrs: false, +}); + +import { isExternal } from "@/utils/index"; + +const props = defineProps({ + to: { + type: Object, + required: true, + }, +}); + +const isExternalLink = computed(() => { + return isExternal(props.to.path || ""); +}); + +const linkType = computed(() => (isExternalLink.value ? "a" : "router-link")); + +const linkProps = (to: any) => { + if (isExternalLink.value) { + return { + href: to.path, + target: "_blank", + rel: "noopener noreferrer", + }; + } + return { to: to }; +}; +</script> diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..3a413b3 --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,86 @@ +<template> + <el-breadcrumb class="flex-y-center"> + <transition-group enter-active-class="animate__animated animate__fadeInRight"> + <el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path"> + <span + v-if="item.redirect === 'noredirect' || index === breadcrumbs.length - 1" + class="color-gray-400" + > + {{ item.meta.title }} + </span> + <a v-else @click.prevent="handleLink(item)"> + {{ item.meta.title }} + </a> + </el-breadcrumb-item> + </transition-group> + </el-breadcrumb> +</template> + +<script setup lang="ts"> +import { RouteLocationMatched } from "vue-router"; +import { compile } from "path-to-regexp"; +import router from "@/router"; + +const currentRoute = useRoute(); +const pathCompile = (path: string) => { + const { params } = currentRoute; + const toPath = compile(path); + return toPath(params); +}; + +const breadcrumbs = ref<Array<RouteLocationMatched>>([]); + +function getBreadcrumb() { + let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title); + const first = matched[0]; + if (!isDashboard(first)) { + matched = [{ path: "/dashboard", meta: { title: "棣栭〉" } } as any].concat(matched); + } + breadcrumbs.value = matched.filter((item) => { + return item.meta && item.meta.title && item.meta.breadcrumb !== false; + }); +} + +function isDashboard(route: RouteLocationMatched) { + const name = route && route.name; + if (!name) { + return false; + } + return name.toString().trim().toLocaleLowerCase() === "Dashboard".toLocaleLowerCase(); +} + +function handleLink(item: any) { + const { redirect, path } = item; + if (redirect) { + router.push(redirect).catch((err) => { + console.warn(err); + }); + return; + } + router.push(pathCompile(path)).catch((err) => { + console.warn(err); + }); +} + +watch( + () => currentRoute.path, + (path) => { + if (path.startsWith("/redirect/")) { + return; + } + getBreadcrumb(); + } +); + +onBeforeMount(() => { + getBreadcrumb(); +}); +</script> + +<style lang="scss" scoped> +// 瑕嗙洊 element-plus 鐨勬牱寮� +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} +</style> diff --git a/src/components/Dict/DictLabel.vue b/src/components/Dict/DictLabel.vue new file mode 100644 index 0000000..b68ee15 --- /dev/null +++ b/src/components/Dict/DictLabel.vue @@ -0,0 +1,60 @@ +<template> + <template v-if="tagType"> + <el-tag :type="tagType" :size="tagSize">{{ label }}</el-tag> + </template> + <template v-else> + <span>{{ label }}</span> + </template> +</template> +<script setup lang="ts"> +import { useDictStore } from "@/store"; + +const props = defineProps({ + code: String, // 瀛楀吀缂栫爜 + modelValue: [String, Number], // 瀛楀吀椤圭殑鍊� + size: { + type: String, + default: "default", // 鏍囩澶у皬 + }, +}); +const label = ref(""); +const tagType = ref<"success" | "warning" | "info" | "primary" | "danger" | undefined>(); // 鏍囩绫诲瀷 +const tagSize = ref<"default" | "large" | "small">(props.size as "default" | "large" | "small"); // 鏍囩澶у皬 + +const dictStore = useDictStore(); +/** + * 鏍规嵁瀛楀吀椤圭殑鍊艰幏鍙栧搴旂殑 label 鍜� tagType + * @param dictCode 瀛楀吀缂栫爜 + * @param value 瀛楀吀椤圭殑鍊� + * @returns 鍖呭惈 label 鍜� tagType 鐨勫璞� + */ +const getLabelAndTagByValue = async (dictCode: string, value: any) => { + // 鎸夐渶鍔犺浇瀛楀吀鏁版嵁 + await dictStore.loadDictItems(dictCode); + // 浠庣紦瀛樹腑鑾峰彇瀛楀吀鏁版嵁 + const dictItems = dictStore.getDictItems(dictCode); + // 鏌ユ壘瀵瑰簲鐨勫瓧鍏搁」 + const dictItem = dictItems.find((item) => item.value == value); + return { + label: dictItem?.label || "", + tagType: dictItem?.tagType, + }; +}; +/** + * 鏇存柊 label 鍜� tagType + */ +const updateLabelAndTag = async () => { + console.log("updateLabelAndTag", props.code, props.modelValue); + if (!props.code || props.modelValue === undefined) return; + const { label: newLabel, tagType: newTagType } = await getLabelAndTagByValue( + props.code, + props.modelValue + ); + label.value = newLabel; + tagType.value = newTagType as typeof tagType.value; +}; + +watch([() => props.code, () => props.modelValue], updateLabelAndTag); + +onMounted(updateLabelAndTag); +</script> diff --git a/src/components/Dict/index.vue b/src/components/Dict/index.vue new file mode 100644 index 0000000..14dbc9c --- /dev/null +++ b/src/components/Dict/index.vue @@ -0,0 +1,133 @@ +<template> + <el-select + v-if="type === 'select'" + v-model="selectedValue" + :placeholder="placeholder" + :disabled="disabled" + clearable + :style="style" + @change="handleChange" + > + <el-option + v-for="option in options" + :key="option.value" + :label="option.label" + :value="option.value" + /> + </el-select> + + <el-radio-group + v-else-if="type === 'radio'" + v-model="selectedValue" + :disabled="disabled" + :style="style" + @change="handleChange" + > + <el-radio + v-for="option in options" + :key="option.value" + :label="option.label" + :value="option.value" + > + {{ option.label }} + </el-radio> + </el-radio-group> + + <el-checkbox-group + v-else-if="type === 'checkbox'" + v-model="selectedValue" + :disabled="disabled" + :style="style" + @change="handleChange" + > + <el-checkbox + v-for="option in options" + :key="option.value" + :label="option.label" + :value="option.value" + > + {{ option.label }} + </el-checkbox> + </el-checkbox-group> +</template> + +<script setup lang="ts"> +import { useDictStore } from "@/store"; + +const dictStore = useDictStore(); + +const props = defineProps({ + code: { + type: String, + required: true, + }, + modelValue: { + type: [String, Number, Array], + required: false, + }, + type: { + type: String, + default: "select", + validator: (value: string) => ["select", "radio", "checkbox"].includes(value), + }, + placeholder: { + type: String, + default: "璇烽�夋嫨", + }, + disabled: { + type: Boolean, + default: false, + }, + style: { + type: Object, + default: () => { + return { + width: "300px", + }; + }, + }, +}); + +const emit = defineEmits(["update:modelValue"]); + +const options = ref<Array<{ label: string; value: string | number }>>([]); + +const selectedValue = ref<any>( + typeof props.modelValue === "string" || typeof props.modelValue === "number" + ? props.modelValue + : Array.isArray(props.modelValue) + ? props.modelValue + : undefined +); + +// 鐩戝惉 modelValue 鍜� options 鐨勫彉鍖� +watch( + [() => props.modelValue, () => options.value], + ([newValue, newOptions]) => { + if (newOptions.length > 0 && newValue !== undefined) { + if (props.type === "checkbox") { + selectedValue.value = Array.isArray(newValue) ? newValue : []; + } else { + const matchedOption = newOptions.find( + (option) => String(option.value) === String(newValue) + ); + selectedValue.value = matchedOption?.value; + } + } else { + selectedValue.value = undefined; + } + }, + { immediate: true } +); + +// 鐩戝惉 selectedValue 鐨勫彉鍖栧苟瑙﹀彂 update:modelValue +function handleChange(val: any) { + emit("update:modelValue", val); +} + +// 鑾峰彇瀛楀吀鏁版嵁 +onMounted(async () => { + await dictStore.loadDictItems(props.code); + options.value = dictStore.getDictItems(props.code); +}); +</script> diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue new file mode 100644 index 0000000..bd888fe --- /dev/null +++ b/src/components/Fullscreen/index.vue @@ -0,0 +1,11 @@ +<template> + <div @click="toggle"> + <div :class="`i-svg:` + (isFullscreen ? 'fullscreen-exit' : 'fullscreen')" /> + </div> +</template> + +<script setup lang="ts"> +const { isFullscreen, toggle } = useFullscreen(); +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..5ee06be --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,60 @@ +<template> + <div class="hamburger-wrapper" @click="toggleClick"> + <div :class="['i-svg:collapse', { hamburger: true, 'is-active': isActive }, hamburgerClass]" /> + </div> +</template> + +<script setup lang="ts"> +import { useSettingsStore } from "@/store"; +import { ThemeMode } from "@/enums/settings/theme.enum"; +import { LayoutMode } from "@/enums/settings/layout.enum"; + +defineProps({ + isActive: { type: Boolean, required: true }, +}); + +const emit = defineEmits(["toggleClick"]); + +const settingsStore = useSettingsStore(); +const layout = computed(() => settingsStore.layout); + +const hamburgerClass = computed(() => { + // 濡傛灉鏆楅粦涓婚 + if (settingsStore.theme === ThemeMode.DARK) { + return "hamburger--white"; + } + + // 濡傛灉鏄贩鍚堝竷灞� && 渚ц竟鏍忛厤鑹叉柟妗堟槸缁忓吀钃� + if (layout.value === LayoutMode.MIX) { + return "hamburger--white"; + } +}); + +function toggleClick() { + emit("toggleClick"); +} +</script> + +<style scoped lang="scss"> +.hamburger-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 0 15px; + cursor: pointer; + + .hamburger { + vertical-align: middle; + transform: scaleX(-1); + transition: transform 0.3s ease; + + &--white { + color: #fff; + } + + &.is-active { + transform: scaleX(1); + } + } +} +</style> diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue new file mode 100644 index 0000000..b96e93e --- /dev/null +++ b/src/components/IconSelect/index.vue @@ -0,0 +1,207 @@ +<template> + <div ref="iconSelectRef" :style="{ width: props.width }"> + <el-popover :visible="popoverVisible" :width="props.width" placement="bottom-end"> + <template #reference> + <div @click="popoverVisible = !popoverVisible"> + <slot> + <el-input v-model="selectedIcon" readonly placeholder="鐐瑰嚮閫夋嫨鍥炬爣" class="reference"> + <template #prepend> + <!-- 鏍规嵁鍥炬爣绫诲瀷灞曠ず --> + <el-icon v-if="isElementIcon"> + <component :is="selectedIcon.replace('el-icon-', '')" /> + </el-icon> + <template v-else> + <div :class="`i-svg:${selectedIcon}`" /> + </template> + </template> + <template #suffix> + <!-- 娓呯┖鎸夐挳 --> + <el-icon + v-if="selectedIcon" + style="margin-right: 8px" + @click.stop="clearSelectedIcon" + > + <CircleClose /> + </el-icon> + + <el-icon + :style="{ + transform: popoverVisible ? 'rotate(180deg)' : 'rotate(0)', + transition: 'transform .5s', + }" + > + <ArrowDown @click.stop="togglePopover" /> + </el-icon> + </template> + </el-input> + </slot> + </div> + </template> + + <!-- 鍥炬爣閫夋嫨寮圭獥 --> + <div ref="popoverContentRef"> + <el-input v-model="filterText" placeholder="鎼滅储鍥炬爣" clearable @input="filterIcons" /> + <el-tabs v-model="activeTab" @tab-click="handleTabClick"> + <el-tab-pane label="SVG 鍥炬爣" name="svg"> + <el-scrollbar height="300px"> + <ul class="icon-grid"> + <li + v-for="icon in filteredSvgIcons" + :key="'svg-' + icon" + class="icon-grid-item" + @click="selectIcon(icon)" + > + <el-tooltip :content="icon" placement="bottom" effect="light"> + <div :class="`i-svg:${icon}`" /> + </el-tooltip> + </li> + </ul> + </el-scrollbar> + </el-tab-pane> + <el-tab-pane label="Element 鍥炬爣" name="element"> + <el-scrollbar height="300px"> + <ul class="icon-grid"> + <li + v-for="icon in filteredElementIcons" + :key="icon" + class="icon-grid-item" + @click="selectIcon(icon)" + > + <el-icon> + <component :is="icon" /> + </el-icon> + </li> + </ul> + </el-scrollbar> + </el-tab-pane> + </el-tabs> + </div> + </el-popover> + </div> +</template> + +<script setup lang="ts"> +import * as ElementPlusIconsVue from "@element-plus/icons-vue"; + +const props = defineProps({ + modelValue: { + type: String, + default: "", + }, + width: { + type: String, + default: "500px", + }, +}); + +const emit = defineEmits(["update:modelValue"]); + +const iconSelectRef = ref(); +const popoverContentRef = ref(); +const popoverVisible = ref(false); +const activeTab = ref("svg"); + +const svgIcons = ref<string[]>([]); +const elementIcons = ref<string[]>(Object.keys(ElementPlusIconsVue)); +const selectedIcon = defineModel("modelValue", { + type: String, + required: true, + default: "", +}); + +const filterText = ref(""); +const filteredSvgIcons = ref<string[]>([]); +const filteredElementIcons = ref<string[]>(elementIcons.value); +const isElementIcon = computed(() => { + return selectedIcon.value && selectedIcon.value.startsWith("el-icon"); +}); + +function loadIcons() { + const icons = import.meta.glob("../../assets/icons/*.svg"); + for (const path in icons) { + const iconName = path.replace(/.*\/(.*)\.svg$/, "$1"); + svgIcons.value.push(iconName); + } + filteredSvgIcons.value = svgIcons.value; +} + +function handleTabClick(tabPane: any) { + activeTab.value = tabPane.props.name; + filterIcons(); +} + +function filterIcons() { + if (activeTab.value === "svg") { + filteredSvgIcons.value = filterText.value + ? svgIcons.value.filter((icon) => icon.toLowerCase().includes(filterText.value.toLowerCase())) + : svgIcons.value; + } else { + filteredElementIcons.value = filterText.value + ? elementIcons.value.filter((icon) => + icon.toLowerCase().includes(filterText.value.toLowerCase()) + ) + : elementIcons.value; + } +} + +function selectIcon(icon: string) { + const iconName = activeTab.value === "element" ? "el-icon-" + icon : icon; + emit("update:modelValue", iconName); + popoverVisible.value = false; +} + +function togglePopover() { + popoverVisible.value = !popoverVisible.value; +} + +onClickOutside(iconSelectRef, () => (popoverVisible.value = false), { + ignore: [popoverContentRef], +}); + +/** + * 娓呯┖宸查�夊浘鏍� + */ +function clearSelectedIcon() { + selectedIcon.value = ""; +} + +onMounted(() => { + loadIcons(); + if (selectedIcon.value) { + if (elementIcons.value.includes(selectedIcon.value.replace("el-icon-", ""))) { + activeTab.value = "element"; + } else { + activeTab.value = "svg"; + } + } +}); +</script> + +<style scoped lang="scss"> +.reference :deep(.el-input__wrapper), +.reference :deep(.el-input__inner) { + cursor: pointer; +} + +.icon-grid { + display: flex; + flex-wrap: wrap; +} + +.icon-grid-item { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + margin: 4px; + cursor: pointer; + border: 1px solid #dcdfe6; + border-radius: 4px; + transition: all 0.3s; +} + +.icon-grid-item:hover { + border-color: #4080ff; + transform: scale(1.2); +} +</style> diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue new file mode 100644 index 0000000..3a7e53d --- /dev/null +++ b/src/components/Pagination/index.vue @@ -0,0 +1,92 @@ +<template> + <el-scrollbar> + <div :class="{ hidden: hidden }" class="pagination"> + <el-pagination + v-model:current-page="currentPage" + v-model:page-size="pageSize" + :background="background" + :layout="layout" + :page-sizes="pageSizes" + :total="total" + @size-change="handleSizeChange" + @current-change="handleCurrentChange" + /> + </div> + </el-scrollbar> +</template> + +<script setup lang="ts"> +const props = defineProps({ + total: { + required: true, + type: Number as PropType<number>, + default: 0, + }, + pageSizes: { + type: Array as PropType<number[]>, + default() { + return [10, 20, 30, 50]; + }, + }, + layout: { + type: String, + default: "total, sizes, prev, pager, next, jumper", + }, + background: { + type: Boolean, + default: true, + }, + autoScroll: { + type: Boolean, + default: true, + }, + hidden: { + type: Boolean, + default: false, + }, +}); + +const emit = defineEmits(["pagination"]); + +const currentPage = defineModel("page", { + type: Number, + required: true, + default: 1, +}); + +const pageSize = defineModel("limit", { + type: Number, + required: true, + default: 10, +}); + +watch( + () => props.total, + (newVal: number) => { + const lastPage = Math.ceil(newVal / pageSize.value); + if (newVal > 0 && currentPage.value > lastPage) { + currentPage.value = lastPage; + emit("pagination", { page: currentPage.value, limit: pageSize.value }); + } + } +); + +function handleSizeChange(val: number) { + currentPage.value = 1; + emit("pagination", { page: currentPage.value, limit: val }); +} + +function handleCurrentChange(val: number) { + emit("pagination", { page: val, limit: pageSize.value }); +} +</script> + +<style lang="scss" scoped> +.pagination { + padding: 12px; + + &.hidden { + display: none; + } +} +</style> diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..8637b31 --- /dev/null +++ b/src/components/SizeSelect/index.vue @@ -0,0 +1,40 @@ +<template> + <el-tooltip content="甯冨眬澶у皬" effect="dark" placement="bottom"> + <el-dropdown trigger="click" @command="handleSizeChange"> + <div> + <div class="i-svg:size" /> + </div> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + v-for="item of sizeOptions" + :key="item.value" + :disabled="appStore.size == item.value" + :command="item.value" + > + {{ item.label }} + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </el-tooltip> +</template> + +<script setup lang="ts"> +import { ComponentSize } from "@/enums/settings/layout.enum"; +import { useAppStore } from "@/store/modules/app.store"; + +const sizeOptions = computed(() => { + return [ + { label: "榛樿", value: ComponentSize.DEFAULT }, + { label: "澶у瀷", value: ComponentSize.LARGE }, + { label: "灏忓瀷", value: ComponentSize.SMALL }, + ]; +}); + +const appStore = useAppStore(); +function handleSizeChange(size: string) { + appStore.changeSize(size); + ElMessage.success("鍒囨崲甯冨眬澶у皬鎴愬姛"); +} +</script> diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue new file mode 100644 index 0000000..472f293 --- /dev/null +++ b/src/components/Upload/FileUpload.vue @@ -0,0 +1,251 @@ +<!-- 鏂囦欢涓婁紶缁勪欢 --> +<template> + <div> + <el-upload + v-model:file-list="fileList" + :style="props.style" + :before-upload="handleBeforeUpload" + :http-request="handleUpload" + :on-progress="handleProgress" + :on-success="handleSuccess" + :on-error="handleError" + :accept="props.accept" + :limit="props.limit" + multiple + > + <!-- 涓婁紶鏂囦欢鎸夐挳 --> + <el-button type="primary" :disabled="fileList.length >= props.limit"> + {{ props.uploadBtnText }} + </el-button> + + <!-- 鏂囦欢鍒楄〃 --> + <template #file="{ file }"> + <div class="el-upload-list__item-info"> + <a class="el-upload-list__item-name" @click="handleDownload(file)"> + <el-icon><Document /></el-icon> + <span class="el-upload-list__item-file-name">{{ file.name }}</span> + <span class="el-icon--close" @click.stop="handleRemove(file.url!)"> + <el-icon><Close /></el-icon> + </span> + </a> + </div> + </template> + </el-upload> + + <el-progress + :style="{ + display: showProgress ? 'inline-flex' : 'none', + width: '100%', + }" + :percentage="progressPercent" + /> + </div> +</template> +<script lang="ts" setup> +import { + UploadRawFile, + UploadUserFile, + UploadFile, + UploadProgressEvent, + UploadRequestOptions, +} from "element-plus"; + +import FileAPI, { FileInfo } from "@/api/file.api"; + +const props = defineProps({ + /** + * 璇锋眰鎼哄甫鐨勯澶栧弬鏁� + */ + data: { + type: Object, + default: () => { + return {}; + }, + }, + /** + * 涓婁紶鏂囦欢鐨勫弬鏁板悕 + */ + name: { + type: String, + default: "file", + }, + /** + * 鏂囦欢涓婁紶鏁伴噺闄愬埗 + */ + limit: { + type: Number, + default: 10, + }, + /** + * 鍗曚釜鏂囦欢涓婁紶澶у皬闄愬埗(鍗曚綅MB) + */ + maxFileSize: { + type: Number, + default: 10, + }, + /** + * 涓婁紶鏂囦欢绫诲瀷 + */ + accept: { + type: String, + default: "*", + }, + /** + * 涓婁紶鎸夐挳鏂囨湰 + */ + uploadBtnText: { + type: String, + default: "涓婁紶鏂囦欢", + }, + + /** + * 鏍峰紡 + */ + style: { + type: Object, + default: () => { + return { + width: "300px", + }; + }, + }, +}); + +const modelValue = defineModel("modelValue", { + type: [Array] as PropType<FileInfo[]>, + required: true, + default: () => [], +}); + +const fileList = ref([] as UploadFile[]); + +const showProgress = ref(false); +const progressPercent = ref(0); + +// 鐩戝惉 modelValue 杞崲鐢ㄤ簬鏄剧ず鐨� fileList +watch( + modelValue, + (value) => { + fileList.value = value.map((item) => { + const name = item.name ? item.name : item.url?.substring(item.url.lastIndexOf("/") + 1); + return { + name: name, + url: item.url, + status: "success", + uid: getUid(), + } as UploadFile; + }); + }, + { + immediate: true, + } +); + +/** + * 涓婁紶鍓嶆牎楠� + */ +function handleBeforeUpload(file: UploadRawFile) { + // 闄愬埗鏂囦欢澶у皬 + if (file.size > props.maxFileSize * 1024 * 1024) { + ElMessage.warning("涓婁紶鍥剧墖涓嶈兘澶т簬" + props.maxFileSize + "M"); + return false; + } + return true; +} + +/* + * 涓婁紶鏂囦欢 + */ +function handleUpload(options: UploadRequestOptions) { + return new Promise((resolve, reject) => { + const file = options.file; + + const formData = new FormData(); + formData.append(props.name, file); + + // 澶勭悊闄勫姞鍙傛暟 + Object.keys(props.data).forEach((key) => { + formData.append(key, props.data[key]); + }); + + FileAPI.upload(formData) + .then((data) => { + resolve(data); + }) + .catch((error) => { + reject(error); + }); + }); +} + +/** + * 涓婁紶杩涘害 + * + * @param event + */ +const handleProgress = (event: UploadProgressEvent) => { + progressPercent.value = event.percent; +}; + +/** + * 涓婁紶鎴愬姛 + */ +const handleSuccess = (fileInfo: FileInfo) => { + ElMessage.success("涓婁紶鎴愬姛"); + + modelValue.value = [...modelValue.value, { name: fileInfo.name, url: fileInfo.url } as FileInfo]; +}; + +/** + * 涓婁紶澶辫触 + */ +const handleError = (_error: any) => { + console.error(_error); + ElMessage.error("涓婁紶澶辫触"); +}; + +/** + * 鍒犻櫎鏂囦欢 + */ +function handleRemove(fileUrl: string) { + FileAPI.delete(fileUrl).then(() => { + modelValue.value = modelValue.value.filter((file) => file.url !== fileUrl); + }); +} + +/** + * 涓嬭浇鏂囦欢 + */ +function handleDownload(file: UploadUserFile) { + const { url, name } = file; + if (url) { + FileAPI.download(url, name); + } +} + +/** 鑾峰彇涓�涓笉閲嶅鐨刬d */ +function getUid(): number { + // 鏃堕棿鎴冲乏绉�13浣嶏紙鐩稿綋浜庝箻浠�8192锛� + 4浣嶉殢鏈烘暟 + return (Date.now() << 13) | Math.floor(Math.random() * 8192); +} +</script> +<style lang="scss" scoped> +.el-upload-list__item .el-icon--close { + position: absolute; + top: 50%; + right: 5px; + color: var(--el-text-color-regular); + cursor: pointer; + opacity: 0.75; + transform: translateY(-50%); + transition: opacity var(--el-transition-duration); +} + +:deep(.el-upload-list) { + margin: 0; +} + +:deep(.el-upload-list__item) { + margin: 0; +} +</style> diff --git a/src/components/Upload/MultiImageUpload.vue b/src/components/Upload/MultiImageUpload.vue new file mode 100644 index 0000000..aeb2667 --- /dev/null +++ b/src/components/Upload/MultiImageUpload.vue @@ -0,0 +1,215 @@ +<!-- 鍥剧墖涓婁紶缁勪欢 --> +<template> + <el-upload + v-model:file-list="fileList" + list-type="picture-card" + :before-upload="handleBeforeUpload" + :http-request="handleUpload" + :on-success="handleSuccess" + :on-error="handleError" + :on-exceed="handleExceed" + :accept="props.accept" + :limit="props.limit" + multiple + > + <el-icon><Plus /></el-icon> + <template #file="{ file }"> + <div style="width: 100%"> + <img class="el-upload-list__item-thumbnail" :src="file.url" /> + <span class="el-upload-list__item-actions"> + <!-- 棰勮 --> + <span @click="handlePreviewImage(file.url!)"> + <el-icon><zoom-in /></el-icon> + </span> + <!-- 鍒犻櫎 --> + <span @click="handleRemove(file.url!)"> + <el-icon><Delete /></el-icon> + </span> + </span> + </div> + </template> + </el-upload> + + <el-image-viewer + v-if="previewVisible" + :zoom-rate="1.2" + :initial-index="previewImageIndex" + :url-list="modelValue" + @close="handlePreviewClose" + /> +</template> +<script setup lang="ts"> +import { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus"; +import FileAPI, { FileInfo } from "@/api/file.api"; + +const props = defineProps({ + /** + * 璇锋眰鎼哄甫鐨勯澶栧弬鏁� + */ + data: { + type: Object, + default: () => { + return {}; + }, + }, + /** + * 涓婁紶鏂囦欢鐨勫弬鏁板悕 + */ + name: { + type: String, + default: "file", + }, + /** + * 鏂囦欢涓婁紶鏁伴噺闄愬埗 + */ + limit: { + type: Number, + default: 10, + }, + /** + * 鍗曚釜鏂囦欢鐨勬渶澶у厑璁稿ぇ灏� + */ + maxFileSize: { + type: Number, + default: 10, + }, + /** + * 涓婁紶鏂囦欢绫诲瀷 + */ + accept: { + type: String, + default: "image/*", // 榛樿鏀寔鎵�鏈夊浘鐗囨牸寮� 锛屽鏋滈渶瑕佹寚瀹氭牸寮忥紝鏍煎紡濡備笅锛�'.png,.jpg,.jpeg,.gif,.bmp' + }, +}); + +const previewVisible = ref(false); // 鏄惁鏄剧ず棰勮 +const previewImageIndex = ref(0); // 棰勮鍥剧墖鐨勭储寮� + +const modelValue = defineModel("modelValue", { + type: [Array] as PropType<string[]>, + default: () => [], +}); + +const fileList = ref<UploadUserFile[]>([]); + +/** + * 鍒犻櫎鍥剧墖 + */ +function handleRemove(imageUrl: string) { + FileAPI.delete(imageUrl).then(() => { + const index = modelValue.value.indexOf(imageUrl); + if (index !== -1) { + // 鐩存帴淇敼鏁扮粍閬垮厤瑙﹀彂鏁翠綋鏇存柊 + modelValue.value.splice(index, 1); + fileList.value.splice(index, 1); // 鍚屾鏇存柊 fileList + } + }); +} + +/** + * 涓婁紶鍓嶆牎楠� + */ +function handleBeforeUpload(file: UploadRawFile) { + // 鏍¢獙鏂囦欢绫诲瀷锛氳櫧鐒� accept 灞炴�ч檺鍒朵簡鐢ㄦ埛鍦ㄦ枃浠堕�夋嫨鍣ㄤ腑鍙�夌殑鏂囦欢绫诲瀷锛屼絾浠嶉渶鍦ㄤ笂浼犳椂鍐嶆鏍¢獙鏂囦欢瀹為檯绫诲瀷锛岀‘淇濈鍚� accept 鐨勮鍒� + const acceptTypes = props.accept.split(",").map((type) => type.trim()); + + // 妫�鏌ユ枃浠舵牸寮忔槸鍚︾鍚� accept + const isValidType = acceptTypes.some((type) => { + if (type === "image/*") { + // 濡傛灉鏄� image/*锛屾鏌� MIME 绫诲瀷鏄惁浠� "image/" 寮�澶� + return file.type.startsWith("image/"); + } else if (type.startsWith(".")) { + // 濡傛灉鏄墿灞曞悕 (.png, .jpg)锛屾鏌ユ枃浠跺悕鏄惁浠ユ寚瀹氭墿灞曞悕缁撳熬 + return file.name.toLowerCase().endsWith(type); + } else { + // 濡傛灉鏄叿浣撶殑 MIME 绫诲瀷 (image/png, image/jpeg)锛屾鏌ユ槸鍚﹀畬鍏ㄥ尮閰� + return file.type === type; + } + }); + + if (!isValidType) { + ElMessage.warning(`涓婁紶鏂囦欢鐨勬牸寮忎笉姝g‘锛屼粎鏀寔锛�${props.accept}`); + return false; + } + + // 闄愬埗鏂囦欢澶у皬 + if (file.size > props.maxFileSize * 1024 * 1024) { + ElMessage.warning("涓婁紶鍥剧墖涓嶈兘澶т簬" + props.maxFileSize + "M"); + return false; + } + return true; +} + +/* + * 涓婁紶鏂囦欢 + */ +function handleUpload(options: UploadRequestOptions) { + return new Promise((resolve, reject) => { + const file = options.file; + + const formData = new FormData(); + formData.append(props.name, file); + + // 澶勭悊闄勫姞鍙傛暟 + Object.keys(props.data).forEach((key) => { + formData.append(key, props.data[key]); + }); + + FileAPI.upload(formData) + .then((data) => { + resolve(data); + }) + .catch((error) => { + reject(error); + }); + }); +} + +/** + * 涓婁紶鏂囦欢瓒呭嚭闄愬埗 + */ +function handleExceed() { + ElMessage.warning("鏈�澶氬彧鑳戒笂浼�" + props.limit + "寮犲浘鐗�"); +} + +/** + * 涓婁紶鎴愬姛鍥炶皟 + */ +const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => { + ElMessage.success("涓婁紶鎴愬姛"); + const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid); + if (index !== -1) { + fileList.value[index].url = fileInfo.url; + fileList.value[index].status = "success"; + modelValue.value[index] = fileInfo.url; + } +}; + +/** + * 涓婁紶澶辫触鍥炶皟 + */ +const handleError = (error: any) => { + console.log("handleError"); + ElMessage.error("涓婁紶澶辫触: " + error.message); +}; + +/** + * 棰勮鍥剧墖 + */ +const handlePreviewImage = (imageUrl: string) => { + previewImageIndex.value = modelValue.value.findIndex((url) => url === imageUrl); + previewVisible.value = true; +}; + +/** + * 鍏抽棴棰勮 + */ +const handlePreviewClose = () => { + previewVisible.value = false; +}; + +onMounted(() => { + fileList.value = modelValue.value.map((url) => ({ url }) as UploadUserFile); +}); +</script> +<style lang="scss" scoped></style> diff --git a/src/components/Upload/SingleImageUpload.vue b/src/components/Upload/SingleImageUpload.vue new file mode 100644 index 0000000..8da4aae --- /dev/null +++ b/src/components/Upload/SingleImageUpload.vue @@ -0,0 +1,202 @@ +<!-- 鍗曞浘涓婁紶缁勪欢 --> +<template> + <el-upload + v-model="modelValue" + class="single-upload" + list-type="picture-card" + :show-file-list="false" + :accept="props.accept" + :before-upload="handleBeforeUpload" + :http-request="handleUpload" + :on-success="onSuccess" + :on-error="onError" + multiple + > + <template #default> + <el-image v-if="modelValue" :src="modelValue" /> + <el-icon v-if="modelValue" class="single-upload__delete-btn" @click.stop="handleDelete"> + <CircleCloseFilled /> + </el-icon> + <el-icon v-else class="single-upload__add-btn"> + <Plus /> + </el-icon> + </template> + </el-upload> +</template> + +<script setup lang="ts"> +import { UploadRawFile, UploadRequestOptions } from "element-plus"; +import FileAPI, { FileInfo } from "@/api/file.api"; + +const props = defineProps({ + /** + * 璇锋眰鎼哄甫鐨勯澶栧弬鏁� + */ + data: { + type: Object, + default: () => { + return {}; + }, + }, + /** + * 涓婁紶鏂囦欢鐨勫弬鏁板悕 + */ + name: { + type: String, + default: "file", + }, + /** + * 鏈�澶ф枃浠跺ぇ灏忥紙鍗曚綅锛歁锛� + */ + maxFileSize: { + type: Number, + default: 10, + }, + + /** + * 涓婁紶鍥剧墖鏍煎紡锛岄粯璁ゆ敮鎸佹墍鏈夊浘鐗�(image/*)锛屾寚瀹氭牸寮忕ず渚嬶細'.png,.jpg,.jpeg,.gif,.bmp' + */ + accept: { + type: String, + default: "image/*", + }, + + /** + * 鑷畾涔夋牱寮忥紝鐢ㄤ簬璁剧疆缁勪欢鐨勫搴﹀拰楂樺害绛夊叾浠栨牱寮� + */ + style: { + type: Object, + default: () => { + return { + width: "150px", + height: "150px", + }; + }, + }, +}); + +const modelValue = defineModel("modelValue", { + type: String, + default: () => "", +}); + +/** + * 闄愬埗鐢ㄦ埛涓婁紶鏂囦欢鐨勬牸寮忓拰澶у皬 + */ +function handleBeforeUpload(file: UploadRawFile) { + // 鏍¢獙鏂囦欢绫诲瀷锛氳櫧鐒� accept 灞炴�ч檺鍒朵簡鐢ㄦ埛鍦ㄦ枃浠堕�夋嫨鍣ㄤ腑鍙�夌殑鏂囦欢绫诲瀷锛屼絾浠嶉渶鍦ㄤ笂浼犳椂鍐嶆鏍¢獙鏂囦欢瀹為檯绫诲瀷锛岀‘淇濈鍚� accept 鐨勮鍒� + const acceptTypes = props.accept.split(",").map((type) => type.trim()); + + // 妫�鏌ユ枃浠舵牸寮忔槸鍚︾鍚� accept + const isValidType = acceptTypes.some((type) => { + if (type === "image/*") { + // 濡傛灉鏄� image/*锛屾鏌� MIME 绫诲瀷鏄惁浠� "image/" 寮�澶� + return file.type.startsWith("image/"); + } else if (type.startsWith(".")) { + // 濡傛灉鏄墿灞曞悕 (.png, .jpg)锛屾鏌ユ枃浠跺悕鏄惁浠ユ寚瀹氭墿灞曞悕缁撳熬 + return file.name.toLowerCase().endsWith(type); + } else { + // 濡傛灉鏄叿浣撶殑 MIME 绫诲瀷 (image/png, image/jpeg)锛屾鏌ユ槸鍚﹀畬鍏ㄥ尮閰� + return file.type === type; + } + }); + + if (!isValidType) { + ElMessage.warning(`涓婁紶鏂囦欢鐨勬牸寮忎笉姝g‘锛屼粎鏀寔锛�${props.accept}`); + return false; + } + + // 闄愬埗鏂囦欢澶у皬 + if (file.size > props.maxFileSize * 1024 * 1024) { + ElMessage.warning("涓婁紶鍥剧墖涓嶈兘澶т簬" + props.maxFileSize + "M"); + return false; + } + return true; +} + +/* + * 涓婁紶鍥剧墖 + */ +function handleUpload(options: UploadRequestOptions) { + return new Promise((resolve, reject) => { + const file = options.file; + + const formData = new FormData(); + formData.append(props.name, file); + + // 澶勭悊闄勫姞鍙傛暟 + Object.keys(props.data).forEach((key) => { + formData.append(key, props.data[key]); + }); + + FileAPI.upload(formData) + .then((data) => { + resolve(data); + }) + .catch((error) => { + reject(error); + }); + }); +} + +/** + * 鍒犻櫎鍥剧墖 + */ +function handleDelete() { + modelValue.value = ""; +} + +/** + * 涓婁紶鎴愬姛鍥炶皟 + * + * @param fileInfo 涓婁紶鎴愬姛鍚庣殑鏂囦欢淇℃伅 + */ +const onSuccess = (fileInfo: FileInfo) => { + ElMessage.success("涓婁紶鎴愬姛"); + modelValue.value = fileInfo.url; +}; + +/** + * 涓婁紶澶辫触鍥炶皟 + */ +const onError = (error: any) => { + console.log("onError"); + ElMessage.error("涓婁紶澶辫触: " + error.message); +}; +</script> + +<style scoped lang="scss"> +:deep(.el-upload--picture-card) { + /* width: var(--el-upload-picture-card-size); + height: var(--el-upload-picture-card-size); */ + width: v-bind("props.style.width"); + height: v-bind("props.style.height"); +} + +.single-upload { + position: relative; + overflow: hidden; + cursor: pointer; + border: 1px var(--el-border-color) solid; + border-radius: 5px; + + &:hover { + border-color: var(--el-color-primary); + } + + &__delete-btn { + position: absolute; + top: 1px; + right: 1px; + font-size: 16px; + color: #ff7901; + cursor: pointer; + background: #fff; + border-radius: 100%; + + :hover { + color: #ff4500; + } + } +} +</style> diff --git a/src/components/WangEditor/index.vue b/src/components/WangEditor/index.vue new file mode 100644 index 0000000..fe8f893 --- /dev/null +++ b/src/components/WangEditor/index.vue @@ -0,0 +1,87 @@ +<!-- + * 鍩轰簬 wangEditor-next 鐨勫瘜鏂囨湰缂栬緫鍣ㄧ粍浠朵簩娆″皝瑁� + * 鐗堟潈鎵�鏈� 漏 2021-present 鏈夋潵寮�婧愮粍缁� + * + * 寮�婧愬崗璁細https://opensource.org/licenses/MIT + * 椤圭洰鍦板潃锛歨ttps://gitee.com/youlaiorg/vue3-element-admin + * + * 鍦ㄤ娇鐢ㄦ椂锛岃淇濈暀姝ゆ敞閲婏紝鎰熻阿鎮ㄥ寮�婧愮殑鏀寔锛� + --> + +<template> + <div style="z-index: 999; border: 1px solid #ccc"> + <!-- 宸ュ叿鏍� --> + <Toolbar + :editor="editorRef" + mode="simple" + :default-config="toolbarConfig" + style="border-bottom: 1px solid #ccc" + /> + <!-- 缂栬緫鍣� --> + <Editor + v-model="modelValue" + :style="{ height: height, overflowY: 'hidden' }" + :default-config="editorConfig" + mode="simple" + @on-created="handleCreated" + /> + </div> +</template> + +<script setup lang="ts"> +import "@wangeditor-next/editor/dist/css/style.css"; +import { Toolbar, Editor } from "@wangeditor-next/editor-for-vue"; +import { IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor"; + +// 鏂囦欢涓婁紶 API +import FileAPI from "@/api/file.api"; + +// 涓婁紶鍥剧墖鍥炶皟鍑芥暟绫诲瀷 +type InsertFnType = (_url: string, _alt: string, _href: string) => void; + +defineProps({ + height: { + type: String, + default: "500px", + }, +}); +// 鍙屽悜缁戝畾 +const modelValue = defineModel("modelValue", { + type: String, + required: false, +}); + +// 缂栬緫鍣ㄥ疄渚嬶紝蹇呴』鐢� shallowRef锛岄噸瑕侊紒 +const editorRef = shallowRef(); + +// 宸ュ叿鏍忛厤缃� +const toolbarConfig = ref<Partial<IToolbarConfig>>({}); + +// 缂栬緫鍣ㄩ厤缃� +const editorConfig = ref<Partial<IEditorConfig>>({ + placeholder: "璇疯緭鍏ュ唴瀹�...", + MENU_CONF: { + uploadImage: { + customUpload(file: File, insertFn: InsertFnType) { + // 涓婁紶鍥剧墖 + FileAPI.uploadFile(file).then((res) => { + // 鎻掑叆鍥剧墖 + insertFn(res.url, res.name, res.url); + }); + }, + } as any, + }, +}); + +// 璁板綍 editor 瀹炰緥锛岄噸瑕侊紒 +const handleCreated = (editor: any) => { + editorRef.value = editor; +}; + +// 缁勪欢閿�姣佹椂锛屼篃鍙婃椂閿�姣佺紪杈戝櫒锛岄噸瑕侊紒 +onBeforeUnmount(() => { + const editor = editorRef.value; + if (editor == null) return; + editor.destroy(); +}); +</script> diff --git a/src/directive/index.ts b/src/directive/index.ts new file mode 100644 index 0000000..9c22eb6 --- /dev/null +++ b/src/directive/index.ts @@ -0,0 +1,9 @@ +import type { App } from "vue"; + +import { hasPerm } from "./permission"; + +// 鍏ㄥ眬娉ㄥ唽 directive +export function setupDirective(app: App<Element>) { + // 浣� v-hasPerm 鍦ㄦ墍鏈夌粍浠朵腑閮藉彲鐢� + app.directive("hasPerm", hasPerm); +} diff --git a/src/directive/permission/index.ts b/src/directive/permission/index.ts new file mode 100644 index 0000000..6dd2995 --- /dev/null +++ b/src/directive/permission/index.ts @@ -0,0 +1,64 @@ +import type { Directive, DirectiveBinding } from "vue"; + +import { useUserStore } from "@/store"; + +/** + * 鎸夐挳鏉冮檺 + */ +export const hasPerm: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const requiredPerms = binding.value; + + // 鏍¢獙浼犲叆鐨勬潈闄愬�兼槸鍚﹀悎娉� + if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) { + throw new Error( + "闇�瑕佹彁渚涙潈闄愭爣璇嗭紒渚嬪锛歷-has-perm=\"'sys:user:add'\" 鎴� v-has-perm=\"['sys:user:add', 'sys:user:edit']\"" + ); + } + + const { roles, perms } = useUserStore().userInfo; + + // 瓒呯骇绠$悊鍛樻嫢鏈夋墍鏈夋潈闄� + if (roles.includes("ROOT")) { + return; + } + + // 妫�鏌ユ潈闄� + const hasAuth = Array.isArray(requiredPerms) + ? requiredPerms.some((perm) => perms.includes(perm)) + : perms.includes(requiredPerms); + + // 濡傛灉娌℃湁鏉冮檺锛岀Щ闄よ鍏冪礌 + if (!hasAuth && el.parentNode) { + el.parentNode.removeChild(el); + } + }, +}; + +/** + * 瑙掕壊鏉冮檺鎸囦护 + */ +export const hasRole: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const requiredRoles = binding.value; + + // 鏍¢獙浼犲叆鐨勮鑹插�兼槸鍚﹀悎娉� + if (!requiredRoles || (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))) { + throw new Error( + "闇�瑕佹彁渚涜鑹叉爣璇嗭紒渚嬪锛歷-has-role=\"'ADMIN'\" 鎴� v-has-role=\"['ADMIN', 'TEST']\"" + ); + } + + const { roles } = useUserStore().userInfo; + + // 妫�鏌ユ槸鍚︽湁瀵瑰簲瑙掕壊鏉冮檺 + const hasAuth = Array.isArray(requiredRoles) + ? requiredRoles.some((role) => roles.includes(role)) + : roles.includes(requiredRoles); + + // 濡傛灉娌℃湁鏉冮檺锛岀Щ闄ゅ厓绱� + if (!hasAuth && el.parentNode) { + el.parentNode.removeChild(el); + } + }, +}; diff --git a/src/enums/common/result.enum.ts b/src/enums/common/result.enum.ts new file mode 100644 index 0000000..3c31188 --- /dev/null +++ b/src/enums/common/result.enum.ts @@ -0,0 +1,23 @@ +/** + * 鍝嶅簲鐮佹灇涓� + */ +export const enum ResultCode { + /** + * 鎴愬姛 + */ + SUCCESS = "00000", + /** + * 閿欒 + */ + ERROR = "B0001", + + /** + * 璁块棶浠ょ墝鏃犳晥鎴栬繃鏈� + */ + ACCESS_TOKEN_INVALID = "A0230", + + /** + * 鍒锋柊浠ょ墝鏃犳晥鎴栬繃鏈� + */ + REFRESH_TOKEN_INVALID = "A0231", +} diff --git a/src/enums/index.ts b/src/enums/index.ts new file mode 100644 index 0000000..922251f --- /dev/null +++ b/src/enums/index.ts @@ -0,0 +1,8 @@ +export * from "./settings/layout.enum"; +export * from "./settings/theme.enum"; +export * from "./settings/locale.enum"; +export * from "./settings/device.enum"; + +export * from "./common/result.enum"; + +export * from "./system/menu.enum"; diff --git a/src/enums/settings/device.enum.ts b/src/enums/settings/device.enum.ts new file mode 100644 index 0000000..709bcb3 --- /dev/null +++ b/src/enums/settings/device.enum.ts @@ -0,0 +1,14 @@ +/** + * 璁惧鏋氫妇 + */ +export const enum DeviceEnum { + /** + * 瀹藉睆璁惧 + */ + DESKTOP = "desktop", + + /** + * 绐勫睆璁惧 + */ + MOBILE = "mobile", +} diff --git a/src/enums/settings/layout.enum.ts b/src/enums/settings/layout.enum.ts new file mode 100644 index 0000000..e1e4406 --- /dev/null +++ b/src/enums/settings/layout.enum.ts @@ -0,0 +1,53 @@ +/** + * 鑿滃崟甯冨眬鏋氫妇 + */ +export const enum LayoutMode { + /** + * 宸︿晶鑿滃崟甯冨眬 + */ + LEFT = "left", + /** + * 椤堕儴鑿滃崟甯冨眬 + */ + TOP = "top", + + /** + * 娣峰悎鑿滃崟甯冨眬 + */ + MIX = "mix", +} + +/** + * 渚ц竟鏍忕姸鎬佹灇涓� + */ +export const enum SidebarStatus { + /** + * 灞曞紑 + */ + OPENED = "opened", + + /** + * 鍏抽棴 + */ + CLOSED = "closed", +} + +/** + * 缁勪欢灏哄鏋氫妇 + */ +export const enum ComponentSize { + /** + * 榛樿 + */ + DEFAULT = "default", + + /** + * 澶у瀷 + */ + LARGE = "large", + + /** + * 灏忓瀷 + */ + SMALL = "small", +} diff --git a/src/enums/settings/locale.enum.ts b/src/enums/settings/locale.enum.ts new file mode 100644 index 0000000..e3a722a --- /dev/null +++ b/src/enums/settings/locale.enum.ts @@ -0,0 +1,14 @@ +/** + * 璇█鏋氫妇 + */ +export const enum AppLanguage { + /** + * 涓枃 + */ + ZH_CN = "zh-cn", + + /** + * 鑻辨枃 + */ + EN = "en", +} diff --git a/src/enums/settings/theme.enum.ts b/src/enums/settings/theme.enum.ts new file mode 100644 index 0000000..8bfd2f8 --- /dev/null +++ b/src/enums/settings/theme.enum.ts @@ -0,0 +1,18 @@ +/** + * 涓婚鏋氫妇 + */ +export const enum ThemeMode { + /** + * 鏄庝寒涓婚 + */ + LIGHT = "light", + /** + * 鏆楅粦涓婚 + */ + DARK = "dark", + + /** + * 绯荤粺鑷姩 + */ + AUTO = "auto", +} diff --git a/src/enums/system/menu.enum.ts b/src/enums/system/menu.enum.ts new file mode 100644 index 0000000..1795c45 --- /dev/null +++ b/src/enums/system/menu.enum.ts @@ -0,0 +1,35 @@ +// 鏍稿績鏋氫妇瀹氫箟 +export enum MenuTypeEnum { + CATALOG = 2, // 鐩綍 + MENU = 1, // 鑿滃崟 + BUTTON = 3, // 鎸夐挳 + EXTLINK = 4, // 澶栭摼 +} + +// 绫诲瀷鏍囩鏄犲皠閰嶇疆 +export const MenuTypeConfig = { + [MenuTypeEnum.CATALOG]: { + label: "鐩綍", + type: "warning" as const, + icon: "folder-opened", + value: 2, + }, + [MenuTypeEnum.MENU]: { + label: "鑿滃崟", + type: "success" as const, + icon: "menu", + value: 1, + }, + [MenuTypeEnum.BUTTON]: { + label: "鎸夐挳", + type: "danger" as const, + icon: "mouse", + value: 3, + }, + [MenuTypeEnum.EXTLINK]: { + label: "澶栭摼", + type: "info" as const, + icon: "link", + value: 4, + }, +} as const; diff --git a/src/layout/components/AppMain/index.vue b/src/layout/components/AppMain/index.vue new file mode 100644 index 0000000..398ee21 --- /dev/null +++ b/src/layout/components/AppMain/index.vue @@ -0,0 +1,36 @@ +<template> + <section class="app-main" :style="{ height: appMainHeight }"> + <router-view> + <template #default="{ Component, route }"> + <transition enter-active-class="animate__animated animate__fadeIn" mode="out-in"> + <keep-alive :include="cachedViews"> + <component :is="Component" :key="route.path" /> + </keep-alive> + </transition> + </template> + </router-view> + </section> +</template> + +<script setup lang="ts"> +import { useSettingsStore, useTagsViewStore } from "@/store"; +import variables from "@/styles/variables.module.scss"; + +// 缂撳瓨椤甸潰闆嗗悎 +const cachedViews = computed(() => useTagsViewStore().cachedViews); +const appMainHeight = computed(() => { + if (useSettingsStore().tagsView) { + return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`; + } else { + return `calc(100vh - ${variables["navbar-height"]})`; + } +}); +</script> + +<style lang="scss" scoped> +.app-main { + position: relative; + overflow-y: auto; + background-color: var(--el-bg-color-page); +} +</style> diff --git a/src/layout/components/NavBar/components/NavbarRight.vue b/src/layout/components/NavBar/components/NavbarRight.vue new file mode 100644 index 0000000..8193e76 --- /dev/null +++ b/src/layout/components/NavBar/components/NavbarRight.vue @@ -0,0 +1,132 @@ +<template> + <div :class="['navbar__right', navbarRightClass]"> + <!-- 妗岄潰绔樉绀� --> + <template v-if="isDesktop"> + <!-- 鍏ㄥ睆 --> + <Fullscreen /> + + <!-- 甯冨眬澶у皬 --> + <SizeSelect /> + </template> + + <!-- 鐢ㄦ埛澶村儚锛堜釜浜轰腑蹇冦�佹敞閿�鐧诲綍绛夛級 --> + <el-dropdown trigger="click"> + <div class="user-profile"> + <img class="user-profile__avatar" :src="userImg" /> + <span class="user-profile__name">{{ userStore.userInfo.name }}</span> + </div> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item @click="handleProfileClick">涓汉涓績</el-dropdown-item> + <el-dropdown-item divided @click="logout">娉ㄩ攢鐧诲綍</el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + + <!-- 璁剧疆闈㈡澘 --> + <div v-if="defaultSettings.showSettings" @click="settingStore.settingsVisible = true"> + <div class="i-svg:setting" /> + </div> + </div> +</template> +<script setup lang="ts"> +import defaultSettings from "@/settings"; +import { DeviceEnum } from "@/enums/settings/device.enum"; +import { ThemeMode } from "@/enums/settings/theme.enum"; +import { useAppStore, useSettingsStore, useUserStore, useTagsViewStore } from "@/store"; +import userImg from "@/assets/images/user-logo.gif"; + +const appStore = useAppStore(); +const settingStore = useSettingsStore(); +const userStore = useUserStore(); +const tagsViewStore = useTagsViewStore(); + +const route = useRoute(); +const router = useRouter(); +const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP); + +/** + * 鎵撳紑涓汉涓績椤甸潰 + */ +function handleProfileClick() { + router.push({ name: "Profile" }); +} + +// 鏍规嵁涓婚鍜屼晶杈规爮閰嶈壊鏂规閫夋嫨 navbar 鍙充晶鐨勬牱寮忕被 +const navbarRightClass = computed(() => { + // 濡傛灉鏆楅粦涓婚 + if (settingStore.theme === ThemeMode.DARK) { + return "navbar__right--white"; + } +}); + +/** + * 娉ㄩ攢鐧诲綍 + */ +function logout() { + ElMessageBox.confirm("纭畾娉ㄩ攢骞堕��鍑虹郴缁熷悧锛�", "鎻愮ず", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + lockScroll: false, + }).then(() => { + userStore + .logout() + .then(() => { + tagsViewStore.delAllViews(); + }) + .then(() => { + router.push(`/login?redirect=${route.fullPath}`); + }); + }); +} +</script> + +<style lang="scss" scoped> +.navbar__right { + display: flex; + align-items: center; + justify-content: center; + + & > * { + display: inline-block; + min-width: 40px; + height: $navbar-height; + line-height: $navbar-height; + color: var(--el-text-color); + text-align: center; + cursor: pointer; + + &:hover { + background: rgb(0 0 0 / 10%); + } + } + + .user-profile { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 13px; + + &__avatar { + width: 32px; + height: 32px; + border-radius: 50%; + } + + &__name { + margin-left: 10px; + } + } +} + +.layout-top .navbar__right > *, +.layout-mix .navbar__right > * { + color: #fff; +} + +.dark .navbar__right > *:hover { + color: #ccc; +} +</style> diff --git a/src/layout/components/NavBar/index.vue b/src/layout/components/NavBar/index.vue new file mode 100644 index 0000000..9e8c61a --- /dev/null +++ b/src/layout/components/NavBar/index.vue @@ -0,0 +1,40 @@ +<template> + <div class="navbar"> + <div class="navbar__left"> + <!-- 灞曞紑/鏀剁缉鑿滃崟 --> + <Hamburger :is-active="isSidebarOpened" @toggle-click="toggleSideBar" /> + <!-- 闈㈠寘灞� --> + <breadcrumb /> + </div> + <!-- 瀵艰埅鏍忓彸渚� --> + <NavbarRight /> + </div> +</template> + +<script setup lang="ts"> +import { useAppStore } from "@/store"; + +const appStore = useAppStore(); + +// 渚ц竟鏍忔槸鍚︽墦寮� +const isSidebarOpened = computed(() => appStore.sidebar.opened); + +// 灞曞紑/鏀剁缉鑿滃崟 +function toggleSideBar() { + appStore.toggleSidebar(); +} +</script> + +<style lang="scss" scoped> +.navbar { + display: flex; + justify-content: space-between; + height: $navbar-height; + background: var(--el-bg-color); + + &__left { + display: flex; + align-items: center; + } +} +</style> diff --git a/src/layout/components/Settings/components/LayoutSelect.vue b/src/layout/components/Settings/components/LayoutSelect.vue new file mode 100644 index 0000000..a61ac91 --- /dev/null +++ b/src/layout/components/Settings/components/LayoutSelect.vue @@ -0,0 +1,142 @@ +<template> + <div class="layout-select"> + <el-tooltip + v-for="item in layoutOptions" + :key="item.value" + :content="item.label" + placement="bottom" + > + <div + role="button" + tabindex="0" + :class="['layout-item', item.className, { 'is-active': modelValue === item.value }]" + @click="handleLayoutChange(item.value)" + @keydown.enter.space="handleLayoutChange(item.value)" + > + <div class="layout-item-part" /> + <div class="layout-item-part" /> + </div> + </el-tooltip> + </div> +</template> + +<script lang="ts" setup> +import { LayoutMode } from "@/enums/settings/layout.enum"; + +interface LayoutOption { + value: LayoutMode; + label: string; + className: string; +} + +const layoutOptions: LayoutOption[] = [ + { value: LayoutMode.LEFT, label: "宸︿晶妯″紡", className: "left" }, + { value: LayoutMode.TOP, label: "椤堕儴妯″紡", className: "top" }, + { value: LayoutMode.MIX, label: "娣峰悎妯″紡", className: "mix" }, +]; + +const modelValue = defineModel<LayoutMode>("modelValue", { + required: true, + default: () => LayoutMode.LEFT, +}); + +function handleLayoutChange(layout: LayoutMode) { + modelValue.value = layout; +} +</script> + +<style scoped lang="scss"> +.layout-select { + display: flex; + gap: 10px; + justify-content: space-evenly; + padding: 10px 0; + + --layout-primary: #1b2a47; + --layout-background: #f0f2f5; + --layout-shadow: 0 0 8px rgb(0 0 0 / 10%); + --layout-hover: #e3f1f9; +} + +.layout-item { + position: relative; + width: 18%; + height: 50px; + cursor: pointer; + background: var(--layout-background); + border-radius: 8px; + box-shadow: var(--layout-shadow); + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; + + &:hover { + background-color: var(--layout-hover); + transform: scale(1.02); /* 绋嶅井鏀惧ぇ锛岄伩鍏嶈繃浜庡じ寮� */ + } + + &:focus-visible { + outline: 2px solid var(--el-color-primary); + } + + &-part { + position: absolute; + background: var(--layout-primary); + border-radius: 4px; /* 淇濇寔鍜岀埗瀹瑰櫒涓�鑷寸殑鍦嗚 */ + box-shadow: var(--layout-shadow); + transition: all 0.3s ease; + } + + &.left { + .layout-item-part { + &:first-child { + width: 30%; + height: 100%; + border-radius: 4px 0 0 4px; /* 宸﹁竟閮ㄥ垎鍦嗚 */ + } + + &:last-child { + top: 0; + right: 0; + width: 70%; + height: 30%; + background: #fff; + border-radius: 0 4px 4px 0; /* 鍙宠竟閮ㄥ垎鍦嗚 */ + } + } + } + + &.top { + .layout-item-part:first-child { + width: 100%; + height: 30%; + border-radius: 4px 4px 0 0; /* 椤堕儴閮ㄥ垎鍦嗚 */ + } + } + + &.mix { + .layout-item-part { + &:first-child { + width: 100%; + height: 30%; + border-radius: 4px 4px 0 0; /* 椤堕儴閮ㄥ垎鍦嗚 */ + } + + &:last-child { + bottom: 0; + left: 0; + width: 30%; + height: 70%; + border-radius: 0 0 4px 4px; /* 搴曢儴閮ㄥ垎鍦嗚 */ + } + } + } +} + +.is-active { + background-color: var(--layout-hover); + border: 2px solid var(--el-color-primary); + transform: scale(1.05); /* 杞诲井鏀惧ぇ */ +} +</style> diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..b047d5d --- /dev/null +++ b/src/layout/components/Settings/index.vue @@ -0,0 +1,162 @@ +<template> + <el-drawer v-model="drawerVisible" size="300" title="椤圭洰閰嶇疆" :before-close="handleCloseDrawer"> + <!-- 涓婚璁剧疆 --> + <section class="config-section"> + <el-divider>涓婚</el-divider> + + <div class="flex-center config-item"> + <el-switch + v-model="isDarkTheme" + active-icon="Moon" + inactive-icon="Sunny" + @change="handleThemeChange" + /> + </div> + </section> + + <!-- 鐣岄潰璁剧疆 --> + <section class="config-section"> + <el-divider>鐣岄潰璁剧疆</el-divider> + + <div class="config-item flex-x-between"> + <span class="text-xs">涓婚棰滆壊</span> + + <el-color-picker + v-model="selectedThemeColor" + :predefine="colorPresets" + popper-class="theme-picker-dropdown" + /> + </div> + + <div class="config-item flex-x-between"> + <span class="text-xs">寮�鍚� Tags-View</span> + <el-switch v-model="settingsStore.tagsView" /> + </div> + + <div class="config-item flex-x-between"> + <span class="text-xs">渚ц竟鏍� LOGO</span> + <el-switch v-model="settingsStore.sidebarLogo" /> + </div> + </section> + + <!-- 甯冨眬璁剧疆 --> + <section class="config-section"> + <el-divider>瀵艰埅鏍忚缃�</el-divider> + <LayoutSelect v-model="settingsStore.layout" @update:model-value="handleLayoutChange" /> + </section> + </el-drawer> +</template> + +<script setup lang="ts"> +import { LayoutMode } from "@/enums/settings/layout.enum"; +import { ThemeMode } from "@/enums/settings/theme.enum"; +import { useSettingsStore, usePermissionStore, useAppStore } from "@/store"; + +// 棰滆壊棰勮 +const colorPresets = [ + "#4080FF", + "#ff4500", + "#ff8c00", + "#90ee90", + "#00ced1", + "#1e90ff", + "#c71585", + "rgb(255, 120, 0)", + "hsva(120, 40, 94)", +]; + +const route = useRoute(); +const appStore = useAppStore(); +const settingsStore = useSettingsStore(); +const permissionStore = usePermissionStore(); + +const isDarkTheme = ref<boolean>(settingsStore.theme === ThemeMode.DARK); + +const selectedThemeColor = computed({ + get: () => settingsStore.themeColor, + set: (value) => settingsStore.changeThemeColor(value), +}); + +const drawerVisible = computed({ + get: () => settingsStore.settingsVisible, + set: (value) => (settingsStore.settingsVisible = value), +}); + +/** + * 澶勭悊涓婚鍒囨崲 + * + * @param isDark 鏄惁鍚敤鏆楅粦妯″紡 + */ +const handleThemeChange = (isDark: string | number | boolean) => { + settingsStore.changeTheme(isDark ? ThemeMode.DARK : ThemeMode.LIGHT); +}; + +/** + * 澶勭悊甯冨眬鍒囨崲 + * @param layout - 鏂板竷灞�妯″紡 + */ +const handleLayoutChange = (layout: LayoutMode) => { + settingsStore.changeLayout(layout); + if (layout === LayoutMode.MIX && route.name) { + const topLevelRoute = findTopLevelRoute(permissionStore.routes, route.name as string); + if (appStore.activeTopMenuPath !== topLevelRoute.path) { + appStore.activeTopMenu(topLevelRoute.path); + } + } +}; + +/** + * 鏌ユ壘璺敱鐨勯《灞傜埗璺敱 + * + * @param tree 鏍戝舰鏁版嵁 + * @param findName 鏌ユ壘鐨勫悕绉� + */ +function findTopLevelRoute(tree: any[], findName: string) { + let parentMap: any = {}; + + function buildParentMap(node: any, parent: any) { + parentMap[node.name] = parent; + + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + buildParentMap(node.children[i], node); + } + } + } + + for (let i = 0; i < tree.length; i++) { + buildParentMap(tree[i], null); + } + + let currentNode = parentMap[findName]; + while (currentNode) { + if (!parentMap[currentNode.name]) { + return currentNode; + } + currentNode = parentMap[currentNode.name]; + } + return null; +} + +/** + * 鍏抽棴鎶藉眽鍓嶇殑鍥炶皟 + */ +const handleCloseDrawer = () => { + settingsStore.settingsVisible = false; +}; +</script> + +<style lang="scss" scoped> +.config-section { + margin-bottom: 24px; + + .config-item { + padding: 12px 0; + border-bottom: 1px solid var(--el-border-color-light); + + &:last-child { + border-bottom: none; + } + } +} +</style> diff --git a/src/layout/components/Sidebar/components/SidebarLogo.vue b/src/layout/components/Sidebar/components/SidebarLogo.vue new file mode 100644 index 0000000..867fedb --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarLogo.vue @@ -0,0 +1,53 @@ +<template> + <div class="logo"> + <transition enter-active-class="animate__animated animate__fadeInLeft"> + <router-link :key="+collapse" class="wh-full flex-center" to="/"> + <img :src="logo" class="w20px h20px" /> + <span v-if="!collapse" class="title"> + {{ defaultSettings.title }} + </span> + </router-link> + </transition> + </div> +</template> + +<script lang="ts" setup> +import defaultSettings from "@/settings"; +import logo from "@/assets/logo.png"; + +defineProps({ + collapse: { + type: Boolean, + required: true, + }, +}); +</script> + +<style lang="scss" scoped> +.logo { + width: 100%; + height: $navbar-height; + background-color: $sidebar-logo-background; + + .title { + flex-shrink: 0; /* 闃叉瀹瑰櫒鍦ㄧ┖闂翠笉瓒虫椂缂╁皬 */ + margin-left: 10px; + font-size: 14px; + font-weight: bold; + color: white; + } +} + +.layout-top, +.layout-mix { + .logo { + width: $sidebar-width; + } + + &.hideSidebar { + .logo { + width: $sidebar-width-collapsed; + } + } +} +</style> diff --git a/src/layout/components/Sidebar/components/SidebarMenu.vue b/src/layout/components/Sidebar/components/SidebarMenu.vue new file mode 100644 index 0000000..5383ff1 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenu.vue @@ -0,0 +1,110 @@ +<template> + <el-menu + ref="menuRef" + :default-active="currentRoute.path" + :collapse="!appStore.sidebar.opened" + :background-color="variables['menu-background']" + :text-color="variables['menu-text']" + :active-text-color="variables['menu-active-text']" + :unique-opened="false" + :collapse-transition="false" + :mode="menuMode" + @open="onMenuOpen" + @close="onMenuClose" + > + <SidebarMenuItem + v-for="route in data" + :key="route.path" + :item="route" + :base-path="resolveFullPath(route.path)" + /> + </el-menu> +</template> + +<script lang="ts" setup> +import path from "path-browserify"; +import type { MenuInstance } from "element-plus"; + +import { LayoutMode } from "@/enums/settings/layout.enum"; +import { useSettingsStore, useAppStore } from "@/store"; +import { isExternal } from "@/utils/index"; + +import variables from "@/styles/variables.module.scss"; + +const props = defineProps({ + data: { + type: Array<any>, + required: true, + default: () => [], + }, + basePath: { + type: String, + required: true, + example: "/system", + }, +}); + +const menuRef = ref<MenuInstance>(); +const settingsStore = useSettingsStore(); +const appStore = useAppStore(); +const currentRoute = useRoute(); + +// 瀛樺偍宸插睍寮�鐨勮彍鍗曢」绱㈠紩 +const expandedMenuIndexes = ref<string[]>([]); + +// 鏍规嵁甯冨眬妯″紡璁剧疆鑿滃崟鐨勬樉绀烘柟寮忥細椤堕儴甯冨眬浣跨敤姘村钩妯″紡锛屽叾浠栦娇鐢ㄥ瀭鐩存ā寮� +const menuMode = computed(() => { + return settingsStore.layout === LayoutMode.TOP ? "horizontal" : "vertical"; +}); + +/** + * 鑾峰彇瀹屾暣璺緞 + * + * @param routePath 褰撳墠璺敱鐨勭浉瀵硅矾寰� /user + * @returns 瀹屾暣鐨勭粷瀵硅矾寰� D://vue3-element-admin/system/user + */ +function resolveFullPath(routePath: string) { + if (isExternal(routePath)) { + return routePath; + } + if (isExternal(props.basePath)) { + return props.basePath; + } + + // 瑙f瀽璺緞锛岀敓鎴愬畬鏁寸殑缁濆璺緞 + return path.resolve(props.basePath, routePath); +} + +/** + * 鎵撳紑鑿滃崟 + * + * @param index 褰撳墠灞曞紑鐨勮彍鍗曢」绱㈠紩 + */ +const onMenuOpen = (index: string) => { + expandedMenuIndexes.value.push(index); +}; + +/** + * 鍏抽棴鑿滃崟 + * + * @param index 褰撳墠鏀惰捣鐨勮彍鍗曢」绱㈠紩 + */ +const onMenuClose = (index: string) => { + expandedMenuIndexes.value = expandedMenuIndexes.value.filter((item) => item !== index); +}; + +/** + * 鐩戝惉鑿滃崟妯″紡鍙樺寲锛氬綋鑿滃崟妯″紡鍒囨崲涓烘按骞虫ā寮忔椂锛屽叧闂墍鏈夊睍寮�鐨勮彍鍗曢」锛� + * 閬垮厤鍦ㄦ按骞虫ā寮忎笅鑿滃崟椤规樉绀洪敊浣嶃�� + * + * @see https://gitee.com/youlaiorg/vue3-element-admin/issues/IAJ1DR + */ +watch( + () => menuMode.value, + () => { + if (menuMode.value === "horizontal") { + expandedMenuIndexes.value.forEach((item) => menuRef.value!.close(item)); + } + } +); +</script> diff --git a/src/layout/components/Sidebar/components/SidebarMenuItem.vue b/src/layout/components/Sidebar/components/SidebarMenuItem.vue new file mode 100644 index 0000000..613ba03 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenuItem.vue @@ -0,0 +1,194 @@ +<template> + <div v-if="!item.meta || !item.meta.hidden"> + <!--銆愬彾瀛愯妭鐐广�戞樉绀哄彾瀛愯妭鐐规垨鍞竴瀛愯妭鐐逛笖鐖惰妭鐐规湭閰嶇疆濮嬬粓鏄剧ず --> + <template + v-if=" + // 鏈厤缃缁堟樉绀猴紝浣跨敤鍞竴瀛愯妭鐐规浛鎹㈢埗鑺傜偣鏄剧ず涓哄彾瀛愯妭鐐� + (!item.meta?.alwaysShow && + hasOneShowingChild(item.children, item) && + (!onlyOneChild.children || onlyOneChild.noShowingChildren)) || + // 鍗充娇閰嶇疆浜嗗缁堟樉绀猴紝浣嗘棤瀛愯妭鐐癸紝涔熸樉绀轰负鍙跺瓙鑺傜偣 + (item.meta?.alwaysShow && !item.children) + " + > + <AppLink + v-if="onlyOneChild.meta" + :to="{ + path: resolvePath(onlyOneChild.path), + query: onlyOneChild.meta.params, + }" + > + <el-menu-item + :index="resolvePath(onlyOneChild.path)" + :class="{ 'submenu-title-noDropdown': !isNest }" + > + <SidebarMenuItemTitle + :icon="onlyOneChild.meta.icon || item.meta?.icon" + :title="onlyOneChild.meta.title" + /> + </el-menu-item> + </AppLink> + </template> + + <!--銆愰潪鍙跺瓙鑺傜偣銆戞樉绀哄惈澶氫釜瀛愯妭鐐圭殑鐖惰彍鍗曪紝鎴栧缁堟樉绀虹殑鍗曞瓙鑺傜偣 --> + <el-sub-menu v-else :index="resolvePath(item.path)" teleported> + <template #title> + <SidebarMenuItemTitle v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" /> + </template> + + <SidebarMenuItem + v-for="child in item.children" + :key="child.path" + :is-nest="true" + :item="child" + :base-path="resolvePath(child.path)" + /> + </el-sub-menu> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "SidebarMenuItem", + inheritAttrs: false, +}); + +import path from "path-browserify"; +import { RouteRecordRaw } from "vue-router"; + +import { isExternal } from "@/utils"; + +const props = defineProps({ + /** + * 褰撳墠璺敱瀵硅薄 + */ + item: { + type: Object as PropType<RouteRecordRaw>, + required: true, + }, + + /** + * 鐖剁骇瀹屾暣璺緞 + */ + basePath: { + type: String, + required: true, + }, + + /** + * 鏄惁涓哄祵濂楄矾鐢� + */ + isNest: { + type: Boolean, + default: false, + }, +}); + +// 鍙鐨勫敮涓�瀛愯妭鐐� +const onlyOneChild = ref(); + +/** + * 妫�鏌ユ槸鍚︿粎鏈変竴涓彲瑙佸瓙鑺傜偣 + * + * @param children 瀛愯矾鐢辨暟缁� + * @param parent 鐖剁骇璺敱 + * @returns 鏄惁浠呮湁涓�涓彲瑙佸瓙鑺傜偣 + */ +function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw) { + // 杩囨护鍑哄彲瑙佸瓙鑺傜偣 + const showingChildren = children.filter((route: RouteRecordRaw) => { + if (!route.meta?.hidden) { + onlyOneChild.value = route; + return true; + } + return false; + }); + + // 浠呮湁涓�涓垨鏃犲瓙鑺傜偣 + if (showingChildren.length === 1) { + return true; + } + + // 鏃犲瓙鑺傜偣鏃讹紝璁剧疆鐖惰妭鐐逛负鍞竴鏄剧ず鑺傜偣 + if (showingChildren.length === 0) { + onlyOneChild.value = { ...parent, path: "", noShowingChildren: true }; + return true; + } + return false; +} + +/** + * 鑾峰彇瀹屾暣璺緞锛岄�傞厤澶栭儴閾炬帴 + * + * @param routePath 璺敱璺緞 + * @returns 缁濆璺緞 + */ +function resolvePath(routePath: string) { + if (isExternal(routePath)) return routePath; + if (isExternal(props.basePath)) return props.basePath; + + // 鎷兼帴鐖惰矾寰勫拰褰撳墠璺緞 + return path.resolve(props.basePath, routePath); +} +</script> + +<style lang="scss"> +.hideSidebar { + .submenu-title-noDropdown { + position: relative; + padding: 0 !important; + + .el-tooltip { + padding: 0 !important; + + .sub-el-icon { + margin-left: 19px; + } + } + + & > span { + display: inline-block; + visibility: hidden; + width: 0; + height: 0; + overflow: hidden; + } + } + + .el-sub-menu { + overflow: hidden; + + & > .el-sub-menu__title { + padding: 0 !important; + + .sub-el-icon { + margin-left: 19px; + } + + .el-sub-menu__icon-arrow { + display: none; + } + } + } + + .el-menu--collapse { + width: $sidebar-width-collapsed; + + .el-sub-menu { + & > .el-sub-menu__title > span { + display: inline-block; + visibility: hidden; + width: 0; + height: 0; + overflow: hidden; + } + } + } +} + +html.dark { + .el-menu-item:hover { + background-color: $menu-hover; + } +} +</style> diff --git a/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue new file mode 100644 index 0000000..5566e50 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue @@ -0,0 +1,50 @@ +<template> + <template v-if="icon"> + <el-icon v-if="isElIcon" class="el-icon"> + <component :is="iconComponent" /> + </el-icon> + <div v-else :class="`i-svg:${icon}`" /> + </template> + <template v-else> + <div class="i-svg:menu" /> + </template> + <!-- 鑿滃崟鏍囬 --> + <span v-if="title" class="ml-1">{{ title }}</span> +</template> + +<script setup lang="ts"> +const props = defineProps<{ + icon?: string; + title?: string; +}>(); + +const isElIcon = computed(() => props.icon?.startsWith("el-icon")); +const iconComponent = computed(() => props.icon?.replace("el-icon-", "")); +</script> + +<style lang="scss" scoped> +.el-icon { + width: 14px !important; + margin-right: 0 !important; + color: currentcolor; +} + +[class^="i-svg:"] { + width: 14px; + height: 14px; + color: currentcolor !important; +} + +.hideSidebar { + .el-sub-menu, + .el-menu-item { + .el-icon { + margin-left: 20px; + } + } + + [class^="i-svg:"] { + margin-left: 20px; + } +} +</style> diff --git a/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue new file mode 100644 index 0000000..14ce0f2 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue @@ -0,0 +1,90 @@ +<!-- 娣峰悎甯冨眬椤堕儴鑿滃崟 --> +<template> + <el-scrollbar> + <el-menu + mode="horizontal" + :default-active="activePath" + :background-color="variables['menu-background']" + :text-color="variables['menu-text']" + :active-text-color="variables['menu-active-text']" + @select="handleMenuSelect" + > + <el-menu-item v-for="route in topMenus" :key="route.path" :index="route.path"> + <template #title> + <template v-if="route.meta && route.meta.icon"> + <el-icon v-if="route.meta.icon.startsWith('el-icon')" class="sub-el-icon"> + <component :is="route.meta.icon.replace('el-icon-', '')" /> + </el-icon> + <div v-else :class="`i-svg:${route.meta.icon}`" /> + </template> + <span v-if="route.path === '/'">棣栭〉</span> + <span v-else-if="route.meta && route.meta.title" class="ml-1"> + {{ route.meta.title }} + </span> + </template> + </el-menu-item> + </el-menu> + </el-scrollbar> +</template> + +<script lang="ts" setup> +import { LocationQueryRaw, RouteRecordRaw } from "vue-router"; +import { usePermissionStore, useAppStore } from "@/store"; +import variables from "@/styles/variables.module.scss"; + +const router = useRouter(); +const appStore = useAppStore(); +const permissionStore = usePermissionStore(); + +// 褰撳墠婵�娲荤殑椤堕儴鑿滃崟璺緞 +const activePath = computed(() => appStore.activeTopMenuPath); + +// 椤堕儴鑿滃崟鍒楄〃 +const topMenus = ref<RouteRecordRaw[]>([]); + +// 鑾峰彇褰撳墠璺敱璺緞鐨勯《閮ㄨ彍鍗曡矾寰� +const activeTopMenuPath = + useRoute().path.split("/").filter(Boolean).length > 1 + ? useRoute().path.match(/^\/[^/]+/)?.[0] || "/" + : "/"; + +// 璁剧疆褰撳墠婵�娲荤殑椤堕儴鑿滃崟璺緞 +appStore.activeTopMenu(activeTopMenuPath); + +/** + * 澶勭悊鑿滃崟鐐瑰嚮浜嬩欢锛屽垏鎹㈤《閮ㄨ彍鍗曞苟鍔犺浇瀵瑰簲鐨勫乏渚ц彍鍗� + * @param routePath 鐐瑰嚮鐨勮彍鍗曡矾寰� + */ +const handleMenuSelect = (routePath: string) => { + appStore.activeTopMenu(routePath); // 璁剧疆婵�娲荤殑椤堕儴鑿滃崟 + permissionStore.setMixedLayoutLeftRoutes(routePath); // 鏇存柊宸︿晶鑿滃崟 + navigateToFirstLeftMenu(permissionStore.mixedLayoutLeftRoutes); // 璺宠浆鍒板乏渚х涓�涓彍鍗� +}; + +/** + * 璺宠浆鍒板乏渚х涓�涓彲璁块棶鐨勮彍鍗� + * @param menus 宸︿晶鑿滃崟鍒楄〃 + */ +const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => { + if (menus.length === 0) return; + + const [firstMenu] = menus; + + // 濡傛灉绗竴涓彍鍗曟湁瀛愯彍鍗曪紝閫掑綊璺宠浆鍒扮涓�涓瓙鑿滃崟 + if (firstMenu.children && firstMenu.children.length > 0) { + navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]); + } else if (firstMenu.name) { + router.push({ + name: firstMenu.name, + query: + typeof firstMenu.meta?.params === "object" + ? (firstMenu.meta.params as LocationQueryRaw) + : undefined, + }); + } +}; + +onMounted(() => { + topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden); +}); +</script> diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..52a8236 --- /dev/null +++ b/src/layout/components/Sidebar/index.vue @@ -0,0 +1,47 @@ +<template> + <div :class="{ 'has-logo': sidebarLogo }"> + <!-- 娣峰悎甯冨眬椤堕儴 --> + <div v-if="isMixLayout" class="flex w-full"> + <SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" /> + <SidebarMixTopMenu class="flex-1" /> + <NavbarRight /> + </div> + + <!-- 椤堕儴甯冨眬椤堕儴 || 宸︿晶甯冨眬宸︿晶 --> + <template v-else> + <SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" /> + <el-scrollbar> + <SidebarMenu :data="permissionStore.routes" base-path="" /> + </el-scrollbar> + + <!-- 椤堕儴甯冨眬瀵艰埅 --> + <NavbarRight v-if="isTopLayout" /> + </template> + </div> +</template> + +<script setup lang="ts"> +import { LayoutMode } from "@/enums/settings/layout.enum"; +import { useSettingsStore, usePermissionStore, useAppStore } from "@/store"; + +import NavbarRight from "../NavBar/components/NavbarRight.vue"; + +const appStore = useAppStore(); +const settingsStore = useSettingsStore(); +const permissionStore = usePermissionStore(); + +const sidebarLogo = computed(() => settingsStore.sidebarLogo); +const layout = computed(() => settingsStore.layout); + +const isMixLayout = computed(() => layout.value === LayoutMode.MIX); +const isTopLayout = computed(() => layout.value === LayoutMode.TOP); +const isSidebarCollapsed = computed(() => !appStore.sidebar.opened); +</script> + +<style lang="scss" scoped> +.has-logo { + .el-scrollbar { + height: calc(100vh - $navbar-height); + } +} +</style> diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..fee40ad --- /dev/null +++ b/src/layout/components/TagsView/index.vue @@ -0,0 +1,433 @@ +<template> + <div class="tags-container"> + <el-scrollbar class="scroll-container" :vertical="false" @wheel="handleScroll"> + <router-link + v-for="tag in visitedViews" + ref="tagRef" + :key="tag.fullPath" + :class="'tags-item ' + (tagsViewStore.isActive(tag) ? 'active' : '')" + :to="{ path: tag.path, query: tag.query }" + @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" + @contextmenu.prevent="openContentMenu(tag, $event)" + > + {{ tag.title }} + <el-icon + v-if="!isAffix(tag)" + class="tag-close-icon" + @click.prevent.stop="closeSelectedTag(tag)" + > + <Close /> + </el-icon> + </router-link> + </el-scrollbar> + + <!-- tag鏍囩鎿嶄綔鑿滃崟 --> + <ul + v-show="contentMenuVisible" + class="contextmenu" + :style="{ left: left + 'px', top: top + 'px' }" + > + <li @click="refreshSelectedTag(selectedTag)"> + <div class="i-svg:refresh" /> + 鍒锋柊 + </li> + <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> + <div class="i-svg:close" /> + 鍏抽棴 + </li> + <li @click="closeOtherTags"> + <div class="i-svg:close_other" /> + 鍏抽棴鍏跺畠 + </li> + <li v-if="!isFirstView()" @click="closeLeftTags"> + <div class="i-svg:close_left" /> + 鍏抽棴宸︿晶 + </li> + <li v-if="!isLastView()" @click="closeRightTags"> + <div class="i-svg:close_right" /> + 鍏抽棴鍙充晶 + </li> + <li @click="closeAllTags(selectedTag)"> + <div class="i-svg:close_all" /> + 鍏抽棴鎵�鏈� + </li> + </ul> + </div> +</template> + +<script setup lang="ts"> +import { useRoute, useRouter, RouteRecordRaw } from "vue-router"; +import { resolve } from "path-browserify"; + +import { usePermissionStore, useTagsViewStore, useSettingsStore, useAppStore } from "@/store"; + +const { proxy } = getCurrentInstance()!; +const router = useRouter(); +const route = useRoute(); + +const permissionStore = usePermissionStore(); +const tagsViewStore = useTagsViewStore(); +const appStore = useAppStore(); + +const { visitedViews } = storeToRefs(tagsViewStore); +const settingsStore = useSettingsStore(); +const layout = computed(() => settingsStore.layout); + +const selectedTag = ref<TagView>({ + path: "", + fullPath: "", + name: "", + title: "", + affix: false, + keepAlive: false, +}); + +const affixTags = ref<TagView[]>([]); +const left = ref(0); +const top = ref(0); + +watch( + route, + () => { + addTags(); + moveToCurrentTag(); + }, + { + immediate: true, //鍒濆鍖栫珛鍗虫墽琛� + } +); + +const contentMenuVisible = ref(false); // 鍙抽敭鑿滃崟鏄惁鏄剧ず +watch(contentMenuVisible, (value) => { + if (value) { + document.body.addEventListener("click", closeContentMenu); + } else { + document.body.removeEventListener("click", closeContentMenu); + } +}); + +/** + * 杩囨护鍑洪渶瑕佸浐瀹氱殑鏍囩 + */ +function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") { + let tags: TagView[] = []; + routes.forEach((route: RouteRecordRaw) => { + const tagPath = resolve(basePath, route.path); + if (route.meta?.affix) { + tags.push({ + path: tagPath, + fullPath: tagPath, + name: String(route.name), + title: route.meta?.title || "no-name", + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + }); + } + if (route.children) { + const tempTags = filterAffixTags(route.children, basePath + route.path); + if (tempTags.length >= 1) { + tags = [...tags, ...tempTags]; + } + } + }); + return tags; +} + +function initTags() { + const tags: TagView[] = filterAffixTags(permissionStore.routes); + affixTags.value = tags; + for (const tag of tags) { + // Must have tag name + if (tag.name) { + tagsViewStore.addVisitedView(tag); + } + } +} + +function addTags() { + if (route.meta.title) { + tagsViewStore.addView({ + name: route.name as string, + title: route.meta.title, + path: route.path, + fullPath: route.fullPath, + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + query: route.query, + }); + } +} + +function moveToCurrentTag() { + // 浣跨敤 nextTick() 鐨勭洰鐨勬槸纭繚鍦ㄦ洿鏂� tagsView 缁勪欢涔嬪墠锛宻crollPaneRef 瀵硅薄宸茬粡婊氬姩鍒颁簡姝g‘鐨勪綅缃�� + nextTick(() => { + for (const tag of visitedViews.value) { + if (tag.path === route.path) { + // when query is different then update + // route.query = { ...route.query, ...tag.query }; + if (tag.fullPath !== route.fullPath) { + tagsViewStore.updateVisitedView({ + name: route.name as string, + title: route.meta.title || "", + path: route.path, + fullPath: route.fullPath, + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + query: route.query, + }); + } + } + } + }); +} + +function isAffix(tag: TagView) { + return tag?.affix; +} + +function isFirstView() { + return ( + selectedTag.value.path === "/dashboard" || + selectedTag.value.fullPath === tagsViewStore.visitedViews[1]?.fullPath + ); +} + +function isLastView() { + return ( + selectedTag.value.fullPath === + tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]?.fullPath + ); +} + +function refreshSelectedTag(view: TagView) { + tagsViewStore.delCachedView(view); + const { fullPath } = view; + nextTick(() => { + router.replace("/redirect" + fullPath); + }); +} + +function closeSelectedTag(view: TagView) { + tagsViewStore.delView(view).then((res: any) => { + if (tagsViewStore.isActive(view)) { + tagsViewStore.toLastView(res.visitedViews, view); + } + }); +} + +function closeLeftTags() { + tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => { + if (!res.visitedViews.find((item: any) => item.path === route.path)) { + tagsViewStore.toLastView(res.visitedViews); + } + }); +} +function closeRightTags() { + tagsViewStore.delRightViews(selectedTag.value).then((res: any) => { + if (!res.visitedViews.find((item: any) => item.path === route.path)) { + tagsViewStore.toLastView(res.visitedViews); + } + }); +} + +function closeOtherTags() { + router.push(selectedTag.value); + tagsViewStore.delOtherViews(selectedTag.value).then(() => { + moveToCurrentTag(); + }); +} + +function closeAllTags(view: TagView) { + tagsViewStore.delAllViews().then((res: any) => { + tagsViewStore.toLastView(res.visitedViews, view); + }); +} + +/** + * 鎵撳紑鍙抽敭鑿滃崟 + */ +function openContentMenu(tag: TagView, e: MouseEvent) { + const menuMinWidth = 105; + + const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left + const offsetWidth = proxy?.$el.offsetWidth; // container width + const maxLeft = offsetWidth - menuMinWidth; // left boundary + const l = e.clientX - offsetLeft + 15; // 15: margin right + + if (l > maxLeft) { + left.value = maxLeft; + } else { + left.value = l; + } + + // 娣峰悎妯″紡涓嬶紝闇�瑕佸噺鍘婚《閮ㄨ彍鍗�(fixed)鐨勯珮搴� + if (layout.value === "mix") { + top.value = e.clientY - 50; + } else { + top.value = e.clientY; + } + + contentMenuVisible.value = true; + selectedTag.value = tag; +} + +/** + * 鍏抽棴鍙抽敭鑿滃崟 + */ +function closeContentMenu() { + contentMenuVisible.value = false; +} + +/** + * 婊氬姩浜嬩欢 + */ +function handleScroll() { + closeContentMenu(); +} + +function findOutermostParent(tree: any[], findName: string) { + let parentMap: any = {}; + + function buildParentMap(node: any, parent: any) { + parentMap[node.name] = parent; + + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + buildParentMap(node.children[i], node); + } + } + } + + for (let i = 0; i < tree.length; i++) { + buildParentMap(tree[i], null); + } + + let currentNode = parentMap[findName]; + while (currentNode) { + if (!parentMap[currentNode.name]) { + return currentNode; + } + currentNode = parentMap[currentNode.name]; + } + + return null; +} + +const againActiveTop = (newVal: string) => { + if (layout.value !== "mix") return; + const parent = findOutermostParent(permissionStore.routes, newVal); + if (appStore.activeTopMenu !== parent.path) { + appStore.activeTopMenu(parent.path); + } +}; +// 濡傛灉鏄贩鍚堟ā寮忥紝鏇存敼selectedTag锛岄渶瑕佸搴旈珮浜殑activeTop +watch( + () => route.name, + (newVal) => { + if (newVal) { + againActiveTop(newVal as string); + } + }, + { + deep: true, + } +); +onMounted(() => { + initTags(); +}); +</script> + +<style lang="scss" scoped> +.tags-container { + width: 100%; + height: $tags-view-height; + background-color: var(--el-bg-color); + border: 1px solid var(--el-border-color-light); + box-shadow: 0 1px 1px var(--el-box-shadow-light); + + .tags-item { + display: inline-block; + padding: 3px 8px; + margin: 4px 0 0 5px; + font-size: 12px; + cursor: pointer; + border: 1px solid var(--el-border-color-light); + + &:hover { + color: var(--el-color-primary); + } + + &:first-of-type { + margin-left: 15px; + } + + &:last-of-type { + margin-right: 15px; + } + + .tag-close-icon { + vertical-align: -0.15em; + cursor: pointer; + border-radius: 50%; + + &:hover { + color: #fff; + background-color: var(--el-color-primary); + } + } + + &.active { + color: #fff; + background-color: var(--el-color-primary); + + &::before { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 5px; + content: ""; + background: #fff; + border-radius: 50%; + } + + .tag-close-icon:hover { + color: var(--el-color-primary); + background-color: var(--el-fill-color-light); + } + } + } +} + +.contextmenu { + position: absolute; + z-index: 99; + font-size: 12px; + background: var(--el-bg-color-overlay); + border-radius: 4px; + box-shadow: var(--el-box-shadow-light); + + li { + padding: 8px 16px; + cursor: pointer; + + &:hover { + background: var(--el-fill-color-light); + } + } +} + +.scroll-container { + position: relative; + width: 100%; + overflow: hidden; + white-space: nowrap; + + .el-scrollbar__bar { + bottom: 0; + } + + .el-scrollbar__wrap { + height: 49px; + } +} +</style> diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 0000000..d790874 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,304 @@ +<template> + <div class="layout" :class="layoutClass"> + <!-- 绉诲姩绔伄缃╁眰 --> + <div v-if="isMobile && isSidebarOpen" class="layout__overlay" @click="handleCloseSidebar" /> + + <!-- 渚ц竟鏍� --> + <Sidebar class="layout__sidebar" /> + + <!-- 娣峰悎甯冨眬 --> + <div v-if="layout === LayoutMode.MIX" class="layout__container"> + <!-- 宸︿晶鑿滃崟鏍� --> + <div class="layout__sidebar--left"> + <el-scrollbar> + <SidebarMenu :data="mixedLayoutLeftRoutes" :base-path="activeTopMenuPath" /> + </el-scrollbar> + <!-- 渚ц竟鏍忓垏鎹㈡寜閽� --> + <div class="layout__sidebar-toggle"> + <Hamburger :is-active="appStore.sidebar.opened" @toggle-click="handleToggleSidebar" /> + </div> + </div> + <!-- 涓诲唴瀹瑰尯鍩� --> + <div :class="{ hasTagsView: isShowTagsView }" class="layout__main"> + <TagsView v-if="isShowTagsView" /> + <AppMain /> + <Settings v-if="defaultSettings.showSettings" /> + <!-- 杩斿洖椤堕儴鎸夐挳 --> + <el-backtop target=".app-main"> + <div class="i-svg:backtop w-6 h-6" /> + </el-backtop> + </div> + </div> + + <!-- 宸︿晶鎴栭《閮ㄥ竷灞�鐨勪富鍐呭鍖� --> + <div v-else :class="{ hasTagsView: isShowTagsView }" class="layout__main"> + <NavBar v-if="layout === LayoutMode.LEFT" /> + <TagsView v-if="isShowTagsView" /> + <AppMain /> + <Settings v-if="defaultSettings.showSettings" /> + <!-- 杩斿洖椤堕儴鎸夐挳 --> + <el-backtop target=".app-main"> + <div class="i-svg:backtop w-6 h-6" /> + </el-backtop> + </div> + </div> +</template> + +<script setup lang="ts"> +// 鐘舵�佺鐞� +import { useAppStore, useSettingsStore, usePermissionStore } from "@/store"; + +// 閰嶇疆 +import defaultSettings from "@/settings"; + +// 鏋氫妇 +import { DeviceEnum } from "@/enums/settings/device.enum"; +import { LayoutMode } from "@/enums/settings/layout.enum"; + +// 缁勪欢 +import NavBar from "./components/NavBar/index.vue"; + +const appStore = useAppStore(); +const settingsStore = useSettingsStore(); +const permissionStore = usePermissionStore(); +const width = useWindowSize().width; + +const WIDTH_DESKTOP = 992; // 鍝嶅簲寮忓竷灞�瀹瑰櫒鍥哄畾瀹藉害锛堝ぇ灞� >=1200px锛屼腑灞� >=992px锛屽皬灞� >=768px锛� + +const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE); // 鏄惁涓虹Щ鍔ㄨ澶� +const isSidebarOpen = computed(() => appStore.sidebar.opened); // 渚ц竟鏍忔槸鍚﹀睍寮� +const isShowTagsView = computed(() => settingsStore.tagsView); // 鏄惁鏄剧ず鏍囩瑙嗗浘 +const layout = computed(() => settingsStore.layout); // 褰撳墠甯冨眬妯″紡锛坙eft銆乼op銆乵ix锛� +const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 椤堕儴鑿滃崟婵�娲昏矾寰� +const mixedLayoutLeftRoutes = computed(() => permissionStore.mixedLayoutLeftRoutes); // 娣峰悎甯冨眬宸︿晶鑿滃崟璺敱 + +// 鐩戝惉椤堕儴鑿滃崟婵�娲昏矾寰勫彉鍖栵紝鏇存柊娣峰悎甯冨眬宸︿晶鑿滃崟璺敱 +watch( + () => activeTopMenuPath.value, + (newVal: string) => { + permissionStore.setMixedLayoutLeftRoutes(newVal); + }, + { deep: true, immediate: true } +); + +// 鐩戝惉绐楀彛瀹藉害鍙樺寲锛岃皟鏁磋澶囩被鍨嬪拰渚ц竟鏍忕姸鎬� +watchEffect(() => { + const isDesktop = width.value >= WIDTH_DESKTOP; + appStore.toggleDevice(isDesktop ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE); + if (isDesktop) { + appStore.openSideBar(); + } else { + appStore.closeSideBar(); + } +}); + +// 鐩戝惉璺敱鍙樺寲锛屽鏋滄槸绉诲姩璁惧涓斾晶杈规爮灞曞紑锛屽垯鍏抽棴渚ц竟鏍� +const route = useRoute(); +watch(route, () => { + if (isMobile.value && isSidebarOpen.value) { + appStore.closeSideBar(); + } +}); + +// 璁$畻灞炴�э細甯冨眬鏍峰紡 +const layoutClass = computed(() => ({ + hideSidebar: !appStore.sidebar.opened, + openSidebar: appStore.sidebar.opened, + mobile: appStore.device === DeviceEnum.MOBILE, + [`layout-${settingsStore.layout}`]: true, +})); + +/** + * 澶勭悊閬僵灞傜偣鍑讳簨浠讹紝鍏抽棴渚ц竟鏍� + */ +function handleCloseSidebar() { + appStore.closeSideBar(); +} + +/** + * 澶勭悊鍒囨崲渚ц竟鏍忕殑灞曞紑/鏀惰捣鐘舵�� + */ +function handleToggleSidebar() { + appStore.toggleSidebar(); +} +</script> +<style lang="scss" scoped> +.layout { + width: 100%; + height: 100%; + + &__overlay { + position: fixed; + top: 0; + left: 0; + z-index: 999; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.3); + } + + &__sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 999; + width: $sidebar-width; + background-color: $menu-background; + transition: width 0.28s; + + :deep(.el-menu) { + border: none; + } + } + + &__main { + position: relative; + height: 100%; + margin-left: $sidebar-width; + overflow-y: auto; + transition: margin-left 0.28s; + + .fixed-header { + position: sticky; + top: 0; + z-index: 9; + transition: width 0.28s; + } + } +} + +// 鍗犱綅绗﹂�夋嫨鍣� +%layout__sidebar--horizontal { + width: 100% !important; + height: $navbar-height; + + :deep(.el-scrollbar) { + flex: 1; + height: $navbar-height; + } + + :deep(.el-menu-item), + :deep(.el-sub-menu__title), + :deep(.el-menu--horizontal) { + height: $navbar-height; + line-height: $navbar-height; + } +} + +.layout-top { + .layout__sidebar { + position: sticky; + display: flex; + @extend %layout__sidebar--horizontal; + } + + .layout__main { + height: calc(100vh - $navbar-height); + margin-left: 0; + } +} + +.layout-mix { + .layout__sidebar { + @extend %layout__sidebar--horizontal; + } + + .layout__container { + display: flex; + height: 100%; + padding-top: $navbar-height; + + .layout__sidebar--left { + position: relative; + width: $sidebar-width; + height: 100%; + background-color: var(--menu-background); + + :deep(.el-scrollbar) { + height: calc(100vh - $navbar-height - 50px); + } + + :deep(.el-menu) { + height: 100%; + border: none; + } + + .layout__sidebar-toggle { + position: absolute; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 50px; + line-height: 50px; + box-shadow: 0 0 6px -2px var(--el-color-primary); + } + } + + .layout__main { + flex: 1; + min-width: 0; + margin-left: 0; + } + } +} + +.hideSidebar { + &.layout-left { + .layout__main { + margin-left: $sidebar-width-collapsed; + } + } + + &.layout-top { + .layout__main { + margin-left: 0; + } + } + + &.layout-mix { + .layout__sidebar { + width: 100%; + } + + .layout__container { + .layout__sidebar--left { + width: $sidebar-width-collapsed; + } + } + } +} + +.layout-left { + &.hideSidebar { + .layout__sidebar { + width: $sidebar-width-collapsed; + } + + .layout__main { + margin-left: $sidebar-width-collapsed; + } + + &.mobile { + .layout__sidebar { + pointer-events: none; + transform: translate3d(-$sidebar-width, 0, 0); + transition-duration: 0.3s; + } + + .layout__main { + margin-left: 0; + } + } + } + &.openSidebar { + &.mobile { + .layout__main { + margin-left: 0; + } + } + } +} +</style> diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0167bf8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import setupPlugins from "@/plugins"; + +// 鏆楅粦涓婚鏍峰紡 +import "element-plus/theme-chalk/dark/css-vars.css"; +// 鏆楅粦妯″紡鑷畾涔夊彉閲� +import "@/styles/dark/css-vars.css"; +import "@/styles/index.scss"; +import "uno.css"; + +// 鍏ㄥ眬寮曞叆 animate.css +import "animate.css"; + +// 鑷姩涓烘煇浜涢粯璁や簨浠讹紙濡� touchstart銆亀heel 绛夛級娣诲姞 { passive: true },鎻愬崌婊氬姩鎬ц兘骞舵秷闄ゆ帶鍒跺彴鐨勯潪琚姩浜嬩欢鐩戝惉璀﹀憡 +import "default-passive-events"; + +const app = createApp(App); +// 娉ㄥ唽鎻掍欢 +app.use(setupPlugins); +app.mount("#app"); diff --git a/src/plugins/icons.ts b/src/plugins/icons.ts new file mode 100644 index 0000000..fa85ba1 --- /dev/null +++ b/src/plugins/icons.ts @@ -0,0 +1,9 @@ +import type { App } from "vue"; +import * as ElementPlusIconsVue from "@element-plus/icons-vue"; + +// 娉ㄥ唽鎵�鏈夊浘鏍� +export function setupElIcons(app: App<Element>) { + for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..74695ff --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,22 @@ +import type { App } from "vue"; + +import { setupDirective } from "@/directive"; +import { setupRouter } from "@/router"; +import { setupStore } from "@/store"; +import { setupElIcons } from "./icons"; +import { setupPermission } from "./permission"; + +export default { + install(app: App<Element>) { + // 鑷畾涔夋寚浠�(directive) + setupDirective(app); + // 璺敱(router) + setupRouter(app); + // 鐘舵�佺鐞�(store) + setupStore(app); + // Element-plus鍥炬爣 + setupElIcons(app); + // 璺敱瀹堝崼 + setupPermission(); + }, +}; diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts new file mode 100644 index 0000000..8f5a07d --- /dev/null +++ b/src/plugins/permission.ts @@ -0,0 +1,88 @@ +import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router"; +import NProgress from "@/utils/nprogress"; +import { getAccessToken } from "@/utils/auth"; +import router from "@/router"; +import { usePermissionStore, useUserStore } from "@/store"; + +export function setupPermission() { + // 鐧藉悕鍗曡矾鐢� + const whiteList = ["/login"]; + + router.beforeEach(async (to, from, next) => { + NProgress.start(); + + const isLogin = !!getAccessToken(); // 鍒ゆ柇鏄惁鐧诲綍 + if (isLogin) { + if (to.path === "/login") { + // 宸茬櫥褰曪紝璁块棶鐧诲綍椤碉紝璺宠浆鍒伴椤� + next({ path: "/" }); + } else { + const permissionStore = usePermissionStore(); + // 鍒ゆ柇璺敱鏄惁鍔犺浇瀹屾垚 + if (permissionStore.isRoutesLoaded) { + if (to.matched.length === 0) { + // 璺敱鏈尮閰嶏紝璺宠浆鍒�404 + next("/404"); + } else { + // 鍔ㄦ�佽缃〉闈㈡爣棰� + const title = (to.params.title as string) || (to.query.title as string); + if (title) { + to.meta.title = title; + } + next(); + } + } else { + try { + // 鐢熸垚鍔ㄦ�佽矾鐢� + const dynamicRoutes = await permissionStore.generateRoutes(); + dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route)); + next({ ...to, replace: true }); + } catch (error) { + console.error(error); + // 璺敱鍔犺浇澶辫触锛岄噸缃� token 骞堕噸瀹氬悜鍒扮櫥褰曢〉 + await useUserStore().clearSessionAndCache(); + redirectToLogin(to, next); + NProgress.done(); + } + } + } + } else { + // 鏈櫥褰曪紝鍒ゆ柇鏄惁鍦ㄧ櫧鍚嶅崟涓� + if (whiteList.includes(to.path)) { + next(); + } else { + // 涓嶅湪鐧藉悕鍗曪紝閲嶅畾鍚戝埌鐧诲綍椤� + redirectToLogin(to, next); + NProgress.done(); + } + } + }); + + // 鍚庣疆瀹堝崼锛屼繚璇佹瘡娆¤矾鐢辫烦杞粨鏉熸椂鍏抽棴杩涘害鏉� + router.afterEach(() => { + NProgress.done(); + }); +} + +// 閲嶅畾鍚戝埌鐧诲綍椤� +function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) { + const params = new URLSearchParams(to.query as Record<string, string>); + const queryString = params.toString(); + const redirect = queryString ? `${to.path}?${queryString}` : to.path; + next(`/login?redirect=${encodeURIComponent(redirect)}`); +} + +/** 鍒ゆ柇鏄惁鏈夋潈闄� */ +export function hasAuth(value: string | string[], type: "button" | "role" = "button") { + const { roles, perms } = useUserStore().userInfo; + + // 瓒呯骇绠$悊鍛� 鎷ユ湁鎵�鏈夋潈闄� + if (type === "button" && roles.includes("ROOT")) { + return true; + } + + const auths = type === "button" ? perms : roles; + return typeof value === "string" + ? auths.includes(value) + : value.some((perm) => auths.includes(perm)); +} diff --git a/src/router/admin.routes.ts b/src/router/admin.routes.ts new file mode 100644 index 0000000..c5a0906 --- /dev/null +++ b/src/router/admin.routes.ts @@ -0,0 +1,33 @@ +import { type RouteVO } from "@/api/system/menu.api"; + +const routes: RouteVO[] = [ + { + path: "/system", + component: "Layout", + redirect: "/system/user", + name: "/system", + meta: { + title: "绯荤粺绠$悊", + icon: "system", + hidden: false, + alwaysShow: false, + }, + children: [ + { + path: "user", + component: "system/user/index", + name: "User", + meta: { + title: "鐢ㄦ埛绠$悊", + icon: "el-icon-User", + hidden: false, + keepAlive: true, + alwaysShow: false, + }, + children: [], + }, + ], + }, +]; + +export default routes; diff --git a/src/router/customer.routes.ts b/src/router/customer.routes.ts new file mode 100644 index 0000000..729e854 --- /dev/null +++ b/src/router/customer.routes.ts @@ -0,0 +1,5 @@ +import { type RouteVO } from "@/api/system/menu.api"; + +const routes: RouteVO[] = []; + +export default routes; diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..b5cdb5b --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,78 @@ +import type { App } from "vue"; +import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router"; + +export const Layout = () => import("@/layout/index.vue"); + +// 闈欐�佽矾鐢� +export const constantRoutes: RouteRecordRaw[] = [ + { + path: "/redirect", + component: Layout, + meta: { hidden: true }, + children: [ + { + path: "/redirect/:path(.*)", + component: () => import("@/views/redirect/index.vue"), + }, + ], + }, + + { + path: "/login", + component: () => import("@/views/login/index.vue"), + meta: { hidden: true }, + }, + + { + path: "/", + name: "/", + component: Layout, + redirect: "/dashboard", + children: [ + { + path: "dashboard", + component: () => import("@/views/dashboard/index.vue"), + name: "Dashboard", + meta: { + title: "杞欢鍖呯鐞�", + icon: "homepage", + affix: true, + keepAlive: true, + }, + }, + { + path: "profile", + name: "Profile", + component: () => import("@/views/profile/index.vue"), + meta: { title: "涓汉涓績", icon: "user", hidden: true }, + }, + { + path: "401", + component: () => import("@/views/error/401.vue"), + meta: { hidden: true }, + }, + { + path: "404", + component: () => import("@/views/error/404.vue"), + meta: { hidden: true }, + }, + ], + }, +]; + +/** + * 鍒涘缓璺敱 + */ +const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes, + // 鍒锋柊鏃讹紝婊氬姩鏉′綅缃繕鍘� + scrollBehavior: () => ({ left: 0, top: 0 }), +}); + +// 鍏ㄥ眬娉ㄥ唽 router +export function setupRouter(app: App<Element>) { + app.use(router); +} + +export default router; diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..94562ec --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,35 @@ +import { AppLanguage, ThemeMode, LayoutMode, ComponentSize } from "./enums"; + +const { pkg } = __APP_INFO__; + +// 妫�鏌ョ敤鎴风殑鎿嶄綔绯荤粺鏄惁浣跨敤娣辫壊妯″紡 +const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + +const defaultSettings: AppSettings = { + // 绯荤粺Title + title: "杞欢绠$悊骞冲彴", + // 绯荤粺鐗堟湰 + version: pkg.version, + // 鏄惁鏄剧ず璁剧疆 + showSettings: false, + // 鏄惁鏄剧ず鏍囩瑙嗗浘 + tagsView: true, + // 鏄惁鏄剧ず渚ц竟鏍廘ogo + sidebarLogo: true, + // 甯冨眬鏂瑰紡锛岄粯璁や负宸︿晶甯冨眬 + layout: LayoutMode.LEFT, + // 涓婚锛屾牴鎹搷浣滅郴缁熺殑鑹插僵鏂规鑷姩閫夋嫨 + theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT, + // 缁勪欢澶у皬 default | medium | small | large + size: ComponentSize.DEFAULT, + // 璇█ + language: AppLanguage.ZH_CN, + // 涓婚棰滆壊 + themeColor: "#4080FF", + // 鏄惁寮�鍚按鍗� + watermarkEnabled: false, + // 姘村嵃鍐呭 + watermarkContent: pkg.name, +}; + +export default defaultSettings; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..9236155 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +import type { App } from "vue"; +import { createPinia } from "pinia"; + +const store = createPinia(); + +// 鍏ㄥ眬娉ㄥ唽 store +export function setupStore(app: App<Element>) { + app.use(store); +} + +export * from "./modules/app.store"; +export * from "./modules/permission.store"; +export * from "./modules/settings.store"; +export * from "./modules/tags-view.store"; +export * from "./modules/user.store"; +export * from "./modules/dict.store"; +export { store }; diff --git a/src/store/modules/app.store.ts b/src/store/modules/app.store.ts new file mode 100644 index 0000000..de20db8 --- /dev/null +++ b/src/store/modules/app.store.ts @@ -0,0 +1,107 @@ +import defaultSettings from "@/settings"; + +// 瀵煎叆 Element Plus 涓嫳鏂囪瑷�鍖� +import zhCn from "element-plus/es/locale/lang/zh-cn"; +import en from "element-plus/es/locale/lang/en"; +import { store } from "@/store"; +import { DeviceEnum } from "@/enums/settings/device.enum"; +import { SidebarStatus } from "@/enums/settings/layout.enum"; + +export const useAppStore = defineStore("app", () => { + // 璁惧绫诲瀷 + const device = useStorage("device", DeviceEnum.DESKTOP); + // 甯冨眬澶у皬 + const size = useStorage("size", defaultSettings.size); + // 璇█ + const language = useStorage("language", defaultSettings.language); + // 渚ц竟鏍忕姸鎬� + const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED); + const sidebar = reactive({ + opened: sidebarStatus.value === SidebarStatus.OPENED, + withoutAnimation: false, + }); + + // 椤堕儴鑿滃崟婵�娲昏矾寰� + const activeTopMenuPath = useStorage("activeTopMenuPath", ""); + + /** + * 鏍规嵁璇█鏍囪瘑璇诲彇瀵瑰簲鐨勮瑷�鍖� + */ + const locale = computed(() => { + if (language?.value == "en") { + return en; + } else { + return zhCn; + } + }); + + // 鍒囨崲渚ц竟鏍� + function toggleSidebar() { + sidebar.opened = !sidebar.opened; + sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED; + } + + // 鍏抽棴渚ц竟鏍� + function closeSideBar() { + sidebar.opened = false; + sidebarStatus.value = SidebarStatus.CLOSED; + } + + // 鎵撳紑渚ц竟鏍� + function openSideBar() { + sidebar.opened = true; + sidebarStatus.value = SidebarStatus.OPENED; + } + + // 鍒囨崲璁惧 + function toggleDevice(val: string) { + device.value = val; + } + + /** + * 鏀瑰彉甯冨眬澶у皬 + * + * @param val 甯冨眬澶у皬 default | small | large + */ + function changeSize(val: string) { + size.value = val; + } + /** + * 鍒囨崲璇█ + * + * @param val + */ + function changeLanguage(val: string) { + language.value = val; + } + /** + * 娣峰悎妯″紡椤堕儴鍒囨崲 + */ + function activeTopMenu(val: string) { + activeTopMenuPath.value = val; + } + return { + device, + sidebar, + language, + locale, + size, + activeTopMenu, + toggleDevice, + changeSize, + changeLanguage, + toggleSidebar, + closeSideBar, + openSideBar, + activeTopMenuPath, + }; +}); + +/** + * 鐢ㄤ簬鍦ㄧ粍浠跺閮紙濡傚湪Pinia Store 涓級浣跨敤 Pinia 鎻愪緵鐨� store 瀹炰緥銆� + * 瀹樻柟鏂囨。瑙i噴浜嗗浣曞湪缁勪欢澶栭儴浣跨敤 Pinia Store锛� + * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component + */ +export function useAppStoreHook() { + return useAppStore(store); +} diff --git a/src/store/modules/dict.store.ts b/src/store/modules/dict.store.ts new file mode 100644 index 0000000..0e1dea1 --- /dev/null +++ b/src/store/modules/dict.store.ts @@ -0,0 +1,55 @@ +import { store } from "@/store"; +import DictAPI, { type DictItemOption } from "@/api/system/dict.api"; + +export const useDictStore = defineStore("dict", () => { + // 瀛楀吀鏁版嵁缂撳瓨 + const dictCache = useStorage<Record<string, DictItemOption[]>>("dict_cache", {}); + // 璇锋眰闃熷垪锛堥槻姝㈤噸澶嶈姹傦級 + const requestQueue: Record<string, Promise<void>> = {}; + /** + * 缂撳瓨瀛楀吀鏁版嵁 + * @param dictCode 瀛楀吀缂栫爜 + * @param data 瀛楀吀椤瑰垪琛� + */ + const cacheDictItems = (dictCode: string, data: DictItemOption[]) => { + dictCache.value[dictCode] = data; + }; + /** + * 鍔犺浇瀛楀吀鏁版嵁锛堝鏋滅紦瀛樹腑娌℃湁鍒欒姹傦級 + * @param dictCode 瀛楀吀缂栫爜 + */ + const loadDictItems = async (dictCode: string) => { + if (dictCache.value[dictCode]) return; + // 闃叉閲嶅璇锋眰 + if (!requestQueue[dictCode]) { + requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => { + cacheDictItems(dictCode, data); + Reflect.deleteProperty(requestQueue, dictCode); + }); + } + await requestQueue[dictCode]; + }; + /** + * 鑾峰彇瀛楀吀椤瑰垪琛� + * @param dictCode 瀛楀吀缂栫爜 + * @returns 瀛楀吀椤瑰垪琛� + */ + const getDictItems = (dictCode: string): DictItemOption[] => { + return dictCache.value[dictCode] || []; + }; + /** + * 娓呯┖瀛楀吀缂撳瓨 + */ + const clearDictCache = () => { + dictCache.value = {}; + }; + return { + loadDictItems, + getDictItems, + clearDictCache, + }; +}); + +export function useDictStoreHook() { + return useDictStore(store); +} diff --git a/src/store/modules/permission.store.ts b/src/store/modules/permission.store.ts new file mode 100644 index 0000000..98b9e2c --- /dev/null +++ b/src/store/modules/permission.store.ts @@ -0,0 +1,113 @@ +import type { RouteRecordRaw } from "vue-router"; +import { constantRoutes } from "@/router"; +import { store, useUserStore } from "@/store"; +import router from "@/router"; + +import { type RouteVO } from "@/api/system/menu.api"; +import adminRoutes from "@/router/admin.routes"; +import customerRoutes from "@/router/customer.routes"; +const modules = import.meta.glob("../../views/**/**.vue"); +const Layout = () => import("@/layout/index.vue"); + +export const usePermissionStore = defineStore("permission", () => { + const { userInfo } = useUserStore(); + // 鍌ㄦ墍鏈夎矾鐢憋紝鍖呮嫭闈欐�佽矾鐢卞拰鍔ㄦ�佽矾鐢� + const routes = ref<RouteRecordRaw[]>([]); + // 娣峰悎妯″紡宸︿晶鑿滃崟璺敱 + const mixedLayoutLeftRoutes = ref<RouteRecordRaw[]>([]); + // 璺敱鏄惁鍔犺浇瀹屾垚 + const isRoutesLoaded = ref(false); + + /** + * 鑾峰彇鍚庡彴鍔ㄦ�佽矾鐢辨暟鎹紝瑙f瀽骞舵敞鍐屽埌鍏ㄥ眬璺敱 + * + * @returns Promise<RouteRecordRaw[]> 瑙f瀽鍚庣殑鍔ㄦ�佽矾鐢卞垪琛� + */ + function generateRoutes() { + return new Promise<RouteRecordRaw[]>((resolve) => { + let dynamicRoutes = []; + if (userInfo.roleId === 2) { + dynamicRoutes = parseDynamicRoutes(adminRoutes); + } else { + dynamicRoutes = parseDynamicRoutes(customerRoutes); + } + routes.value = [...constantRoutes, ...dynamicRoutes]; + isRoutesLoaded.value = true; + resolve(dynamicRoutes); + }); + } + + /** + * 鏍规嵁鐖惰彍鍗曡矾寰勮缃贩鍚堟ā寮忓乏渚ц彍鍗� + * + * @param parentPath 鐖惰彍鍗曠殑璺緞锛岀敤浜庢煡鎵惧搴旂殑鑿滃崟椤� + */ + const setMixedLayoutLeftRoutes = (parentPath: string) => { + const matchedItem = routes.value.find((item) => item.path === parentPath); + if (matchedItem && matchedItem.children) { + mixedLayoutLeftRoutes.value = matchedItem.children; + } + }; + + /** + * 閲嶇疆璺敱 + */ + const resetRouter = () => { + // 浠� router 瀹炰緥涓Щ闄ゅ姩鎬佽矾鐢� + routes.value.forEach((route) => { + if (route.name && !constantRoutes.find((r) => r.name === route.name)) { + router.removeRoute(route.name); + } + }); + + // 娓呯┖鏈湴瀛樺偍鐨勮矾鐢卞拰鑿滃崟鏁版嵁 + routes.value = []; + mixedLayoutLeftRoutes.value = []; + isRoutesLoaded.value = false; + }; + + return { + routes, + mixedLayoutLeftRoutes, + isRoutesLoaded, + generateRoutes, + setMixedLayoutLeftRoutes, + resetRouter, + }; +}); + +/** + * 瑙f瀽鍚庣杩斿洖鐨勮矾鐢辨暟鎹苟杞崲涓� Vue Router 鍏煎鐨勮矾鐢遍厤缃� + * + * @param rawRoutes 鍚庣杩斿洖鐨勫師濮嬭矾鐢辨暟鎹� + * @returns 瑙f瀽鍚庣殑璺敱閰嶇疆鏁扮粍 + */ +const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => { + const parsedRoutes: RouteRecordRaw[] = []; + + rawRoutes.forEach((route) => { + const normalizedRoute = { ...route } as RouteRecordRaw; + + // 澶勭悊缁勪欢璺緞 + normalizedRoute.component = + normalizedRoute.component?.toString() === "Layout" + ? Layout + : modules[`../../views/${normalizedRoute.component}.vue`] || + modules["../../views/error-page/404.vue"]; + + // 閫掑綊瑙f瀽瀛愯矾鐢� + if (normalizedRoute.children) { + normalizedRoute.children = parseDynamicRoutes(route.children); + } + + parsedRoutes.push(normalizedRoute); + }); + + return parsedRoutes; +}; +/** + * 鍦ㄧ粍浠跺浣跨敤 Pinia store 瀹炰緥 @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html + */ +export function usePermissionStoreHook() { + return usePermissionStore(store); +} diff --git a/src/store/modules/settings.store.ts b/src/store/modules/settings.store.ts new file mode 100644 index 0000000..7892ede --- /dev/null +++ b/src/store/modules/settings.store.ts @@ -0,0 +1,75 @@ +import defaultSettings from "@/settings"; +import { ThemeMode } from "@/enums/settings/theme.enum"; +import { LayoutMode } from "@/enums/settings/layout.enum"; +import { generateThemeColors, applyTheme, toggleDarkMode } from "@/utils/theme"; + +type SettingsValue = boolean | string; + +export const useSettingsStore = defineStore("setting", () => { + // 鍩烘湰璁剧疆 + const settingsVisible = ref(false); + // 鏍囩瑙嗗浘 + const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView); + // 渚ц竟鏍� Logo + const sidebarLogo = useStorage<boolean>("sidebarLogo", defaultSettings.sidebarLogo); + // 甯冨眬 + const layout = useStorage<LayoutMode>("layout", defaultSettings.layout as LayoutMode); + // 姘村嵃 + const watermarkEnabled = useStorage<boolean>( + "watermarkEnabled", + defaultSettings.watermarkEnabled + ); + + // 涓婚 + const themeColor = useStorage<string>("themeColor", defaultSettings.themeColor); + const theme = useStorage<string>("theme", defaultSettings.theme); + + // 鐩戝惉涓婚鍙樺寲 + watch( + [theme, themeColor], + ([newTheme, newThemeColor]) => { + toggleDarkMode(newTheme === ThemeMode.DARK); + const colors = generateThemeColors(newThemeColor); + applyTheme(colors); + }, + { immediate: true } + ); + // 璁剧疆鏇存敼鍑芥暟 + const settingsMap: Record<string, Ref<SettingsValue>> = { + tagsView, + sidebarLogo, + layout, + watermarkEnabled, + }; + + function changeSetting({ key, value }: { key: string; value: SettingsValue }) { + const setting = settingsMap[key]; + if (setting) setting.value = value; + } + + function changeTheme(val: string) { + theme.value = val; + } + + function changeThemeColor(color: string) { + themeColor.value = color; + } + + function changeLayout(val: LayoutMode) { + layout.value = val; + } + + return { + settingsVisible, + tagsView, + sidebarLogo, + layout, + themeColor, + theme, + watermarkEnabled, + changeSetting, + changeTheme, + changeThemeColor, + changeLayout, + }; +}); diff --git a/src/store/modules/tags-view.store.ts b/src/store/modules/tags-view.store.ts new file mode 100644 index 0000000..82fb50b --- /dev/null +++ b/src/store/modules/tags-view.store.ts @@ -0,0 +1,254 @@ +export const useTagsViewStore = defineStore("tagsView", () => { + const visitedViews = ref<TagView[]>([]); + const cachedViews = ref<string[]>([]); + const router = useRouter(); + const route = useRoute(); + + /** + * 娣诲姞宸茶闂鍥惧埌宸茶闂鍥惧垪琛ㄤ腑 + */ + function addVisitedView(view: TagView) { + // 濡傛灉宸茬粡瀛樺湪浜庡凡璁块棶鐨勮鍥惧垪琛ㄤ腑锛屽垯涓嶅啀娣诲姞 + if (visitedViews.value.some((v) => v.path === view.path)) { + return; + } + // 濡傛灉瑙嗗浘鏄浐瀹氱殑锛坅ffix锛夛紝鍒欏湪宸茶闂殑瑙嗗浘鍒楄〃鐨勫紑澶存坊鍔� + if (view.affix) { + visitedViews.value.unshift(view); + } else { + // 濡傛灉瑙嗗浘涓嶆槸鍥哄畾鐨勶紝鍒欏湪宸茶闂殑瑙嗗浘鍒楄〃鐨勬湯灏炬坊鍔� + visitedViews.value.push(view); + } + } + + /** + * 娣诲姞缂撳瓨瑙嗗浘鍒扮紦瀛樿鍥惧垪琛ㄤ腑 + */ + function addCachedView(view: TagView) { + const viewName = view.name; + // 濡傛灉缂撳瓨瑙嗗浘鍚嶇О宸茬粡瀛樺湪浜庣紦瀛樿鍥惧垪琛ㄤ腑锛屽垯涓嶅啀娣诲姞 + if (cachedViews.value.includes(viewName)) { + return; + } + + // 濡傛灉瑙嗗浘闇�瑕佺紦瀛橈紙keepAlive锛夛紝鍒欏皢鍏惰矾鐢卞悕绉版坊鍔犲埌缂撳瓨瑙嗗浘鍒楄〃涓� + if (view.keepAlive) { + cachedViews.value.push(viewName); + } + } + + /** + * 浠庡凡璁块棶瑙嗗浘鍒楄〃涓垹闄ゆ寚瀹氱殑瑙嗗浘 + */ + function delVisitedView(view: TagView) { + return new Promise((resolve) => { + for (const [i, v] of visitedViews.value.entries()) { + // 鎵惧埌涓庢寚瀹氳鍥捐矾寰勫尮閰嶇殑瑙嗗浘锛屽湪宸茶闂鍥惧垪琛ㄤ腑鍒犻櫎璇ヨ鍥� + if (v.path === view.path) { + visitedViews.value.splice(i, 1); + break; + } + } + resolve([...visitedViews.value]); + }); + } + + function delCachedView(view: TagView) { + const viewName = view.name; + return new Promise((resolve) => { + const index = cachedViews.value.indexOf(viewName); + if (index > -1) { + cachedViews.value.splice(index, 1); + } + resolve([...cachedViews.value]); + }); + } + function delOtherVisitedViews(view: TagView) { + return new Promise((resolve) => { + visitedViews.value = visitedViews.value.filter((v) => { + return v?.affix || v.path === view.path; + }); + resolve([...visitedViews.value]); + }); + } + + function delOtherCachedViews(view: TagView) { + const viewName = view.name as string; + return new Promise((resolve) => { + const index = cachedViews.value.indexOf(viewName); + if (index > -1) { + cachedViews.value = cachedViews.value.slice(index, index + 1); + } else { + // if index = -1, there is no cached tags + cachedViews.value = []; + } + resolve([...cachedViews.value]); + }); + } + + function updateVisitedView(view: TagView) { + for (let v of visitedViews.value) { + if (v.path === view.path) { + v = Object.assign(v, view); + break; + } + } + } + + function addView(view: TagView) { + addVisitedView(view); + addCachedView(view); + } + + function delView(view: TagView) { + return new Promise((resolve) => { + delVisitedView(view); + delCachedView(view); + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delOtherViews(view: TagView) { + return new Promise((resolve) => { + delOtherVisitedViews(view); + delOtherCachedViews(view); + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delLeftViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path); + if (currIndex === -1) { + return; + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index >= currIndex || item?.affix) { + return true; + } + + const cacheIndex = cachedViews.value.indexOf(item.name); + if (cacheIndex > -1) { + cachedViews.value.splice(cacheIndex, 1); + } + return false; + }); + resolve({ + visitedViews: [...visitedViews.value], + }); + }); + } + + function delRightViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path); + if (currIndex === -1) { + return; + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index <= currIndex || item?.affix) { + return true; + } + }); + resolve({ + visitedViews: [...visitedViews.value], + }); + }); + } + + function delAllViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix); + visitedViews.value = affixTags; + cachedViews.value = []; + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delAllVisitedViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix); + visitedViews.value = affixTags; + resolve([...visitedViews.value]); + }); + } + + function delAllCachedViews() { + return new Promise((resolve) => { + cachedViews.value = []; + resolve([...cachedViews.value]); + }); + } + + /** + * 鍏抽棴褰撳墠tagView + */ + function closeCurrentView() { + const tags: TagView = { + name: route.name as string, + title: route.meta.title as string, + path: route.path, + fullPath: route.fullPath, + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + query: route.query, + }; + delView(tags).then((res: any) => { + if (isActive(tags)) { + toLastView(res.visitedViews, tags); + } + }); + } + + function isActive(tag: TagView) { + return tag.path === route.path; + } + + function toLastView(visitedViews: TagView[], view?: TagView) { + const latestView = visitedViews.slice(-1)[0]; + if (latestView && latestView.fullPath) { + router.push(latestView.fullPath); + } else { + // now the default is to redirect to the home page if there is no tags-view, + // you can adjust it according to your needs. + if (view?.name === "Dashboard") { + // to reload home page + router.replace("/redirect" + view.fullPath); + } else { + router.push("/"); + } + } + } + + return { + visitedViews, + cachedViews, + addVisitedView, + addCachedView, + delVisitedView, + delCachedView, + delOtherVisitedViews, + delOtherCachedViews, + updateVisitedView, + addView, + delView, + delOtherViews, + delLeftViews, + delRightViews, + delAllViews, + delAllVisitedViews, + delAllCachedViews, + closeCurrentView, + isActive, + toLastView, + }; +}); diff --git a/src/store/modules/user.store.ts b/src/store/modules/user.store.ts new file mode 100644 index 0000000..951851a --- /dev/null +++ b/src/store/modules/user.store.ts @@ -0,0 +1,93 @@ +import { store } from "@/store"; +import { usePermissionStoreHook } from "@/store/modules/permission.store"; +import { useDictStoreHook } from "@/store/modules/dict.store"; + +import { setAccessToken, clearToken } from "@/utils/auth"; +import UserApi, { type LoginFormData, LoginResult, UserInfo } from "@/api/user"; + +export const useUserStore = defineStore("user", () => { + const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo); + + /** + * 鐧诲綍 + * + * @returns + * @param loginData + */ + function login(loginData: LoginFormData) { + return new Promise<void>((resolve, reject) => { + const { username: name, password: pwd } = loginData; + UserApi.login(name, pwd) + .then((result: LoginResult) => { + console.log(result); + const { code, data, data2 } = result; + if (code === 1 && data) { + console.log(123); + setAccessToken("fg"); + Object.assign(userInfo.value, { ...data2, ...{ roles: [], perms: [] } }); + if (userInfo.value.roleId === 2) { + userInfo.value.roles = ["admin"]; + userInfo.value.perms = [ + "sys:user:add", + "sys:user:edit", + "sys:user:delete", + "sys:user:upload", + ]; + } else { + userInfo.value.roles = ["customer"]; + } + resolve(); + } else { + reject("鐧诲綍澶辫触"); + } + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 鐧诲嚭 + */ + function logout() { + return new Promise<void>((resolve, reject) => { + UserApi.logout() + .then(() => { + clearSessionAndCache(); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 娓呴櫎鐢ㄦ埛浼氳瘽鍜岀紦瀛� + */ + function clearSessionAndCache() { + return new Promise<void>((resolve) => { + clearToken(); + usePermissionStoreHook().resetRouter(); + useDictStoreHook().clearDictCache(); + resolve(); + }); + } + + return { + userInfo, + login, + logout, + clearSessionAndCache, + }; +}); + +/** + * 鐢ㄤ簬鍦ㄧ粍浠跺閮紙濡傚湪Pinia Store 涓級浣跨敤 Pinia 鎻愪緵鐨� store 瀹炰緥銆� + * 瀹樻柟鏂囨。瑙i噴浜嗗浣曞湪缁勪欢澶栭儴浣跨敤 Pinia Store锛� + * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component + */ +export function useUserStoreHook() { + return useUserStore(store); +} diff --git a/src/styles/dark/css-vars.css b/src/styles/dark/css-vars.css new file mode 100644 index 0000000..823aabf --- /dev/null +++ b/src/styles/dark/css-vars.css @@ -0,0 +1,7 @@ +/* 鏆楅粦妯″紡閫氳繃 CSS 鑷畾涔夊彉閲忥紝瀹樻柟閾炬帴锛歨ttps://element-plus.org/zh-CN/guide/dark-mode.html#%E9%80%9A%E8%BF%87-css */ +html.dark { + .el-table { + /* 鑷畾涔夎〃鏍奸�変腑楂樹寒鏃跺綋鍓嶈鐨勮儗鏅鑹� */ + --el-table-current-row-bg-color: var(--el-fill-color-light); + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..349c84f --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,28 @@ +@use "./reset"; + +.app-container { + padding: 15px; +} + +.search-bar { + padding: 18px 0 0 10px; + margin-bottom: 10px; + background-color: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 4px; + box-shadow: var(--el-box-shadow-light); +} + +.table-container > .el-card__header { + padding: calc(var(--el-card-padding) - 8px) var(--el-card-padding); +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32 160 255); + } +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 0000000..a20f04a --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,76 @@ +*, +::before, +::after { + box-sizing: border-box; + border-color: currentcolor; + border-style: solid; + border-width: 0; +} + +#app { + width: 100%; + height: 100%; +} + +html { + box-sizing: border-box; + width: 100%; + height: 100%; + line-height: 1.5; + tab-size: 4; + text-size-adjust: 100%; +} + +body { + width: 100%; + height: 100%; + margin: 0; + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", + "Microsoft YaHei", "寰蒋闆呴粦", Arial, sans-serif; + line-height: inherit; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizelegibility; +} + +a { + color: inherit; + text-decoration: inherit; +} + +img, +svg { + display: inline-block; +} + +svg { + // 鍥爄con澶у皬琚缃负鍜屽瓧浣撳ぇ灏忎竴鑷达紝鑰宻pan绛夋爣绛剧殑涓嬭竟缂樹細鍜屽瓧浣撶殑鍩虹嚎瀵归綈锛屾晠闇�璁剧疆涓�涓線涓嬬殑鍋忕Щ姣斾緥锛屾潵绾犳瑙嗚涓婄殑鏈榻愭晥鏋� + vertical-align: -0.15em; +} + +ul, +li { + padding: 0; + margin: 0; + list-style: none; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +a, +a:focus, +a:hover { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +a:focus, +a:active, +div:focus { + outline: none; +} diff --git a/src/styles/variables.module.scss b/src/styles/variables.module.scss new file mode 100644 index 0000000..20a624d --- /dev/null +++ b/src/styles/variables.module.scss @@ -0,0 +1,11 @@ +/* stylelint-disable property-no-unknown */ +:export { + sidebar-width: $sidebar-width; + navbar-height: $navbar-height; + tags-view-height: $tags-view-height; + menu-background: $menu-background; + menu-text: $menu-text; + menu-active-text: $menu-active-text; + menu-hover: $menu-hover; +} +/* stylelint-enable property-no-unknown */ diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..7948a7c --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,58 @@ +@forward "element-plus/theme-chalk/src/common/var.scss" with ( + $colors: ( + "primary": ( + "base": #4080ff, + ), + "success": ( + "base": #23c343, + ), + "warning": ( + "base": #ff9a2e, + ), + "danger": ( + "base": #f76560, + ), + "info": ( + "base": #a9aeb8, + ), + ), + + $bg-color: ( + "page": #f5f8fd, + ) +); + +/** 鍏ㄥ眬SCSS鍙橀噺 */ + +:root { + --menu-background: #304156; + --menu-text: #bfcbd9; + --menu-active-text: var(--el-menu-active-color); + --menu-hover: #263445; + --sidebar-logo-background: #2d3748; + + // 淇琛ㄦ牸 fixed 鍒楄閫変腑鍚庣敱浜庨�忔槑鑹插鑷村彔瀛楃殑 bug + .el-table { + --el-table-current-row-bg-color: rgb(235 243 250); + } +} + +/** 鏆楅粦涓婚 */ +html.dark { + --menu-background: var(--el-bg-color-overlay); + --menu-text: #fff; + --menu-active-text: var(--el-menu-active-color); + --menu-hover: rgb(0 0 0 / 20%); + --sidebar-logo-background: rgb(0 0 0 / 20%); +} + +$menu-background: var(--menu-background); // 鑿滃崟鑳屾櫙鑹� +$menu-text: var(--menu-text); // 鑿滃崟鏂囧瓧棰滆壊 +$menu-active-text: var(--menu-active-text); // 鑿滃崟婵�娲绘枃瀛楅鑹� +$menu-hover: var(--menu-hover); // 鑿滃崟鎮仠鑳屾櫙鑹� +$sidebar-logo-background: var(--sidebar-logo-background); // 渚ц竟鏍� Logo 鑳屾櫙鑹� + +$sidebar-width: 210px; // 渚ц竟鏍忓搴� +$sidebar-width-collapsed: 54px; // 渚ц竟鏍忔敹缂╁搴� +$navbar-height: 50px; // 瀵艰埅鏍忛珮搴� +$tags-view-height: 34px; // TagsView 楂樺害 diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts new file mode 100644 index 0000000..86b4a5c --- /dev/null +++ b/src/types/auto-imports.d.ts @@ -0,0 +1,989 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: (typeof import("vue"))["EffectScope"]; + const ElForm: (typeof import("element-plus/es"))["ElForm"]; + const ElMessage: (typeof import("element-plus/es"))["ElMessage"]; + const ElMessageBox: (typeof import("element-plus/es"))["ElMessageBox"]; + const ElNotification: (typeof import("element-plus/es"))["ElNotification"]; + const ElTree: (typeof import("element-plus/es"))["ElTree"]; + const acceptHMRUpdate: (typeof import("pinia"))["acceptHMRUpdate"]; + const asyncComputed: (typeof import("@vueuse/core"))["asyncComputed"]; + const autoResetRef: (typeof import("@vueuse/core"))["autoResetRef"]; + const computed: (typeof import("vue"))["computed"]; + const computedAsync: (typeof import("@vueuse/core"))["computedAsync"]; + const computedEager: (typeof import("@vueuse/core"))["computedEager"]; + const computedInject: (typeof import("@vueuse/core"))["computedInject"]; + const computedWithControl: (typeof import("@vueuse/core"))["computedWithControl"]; + const controlledComputed: (typeof import("@vueuse/core"))["controlledComputed"]; + const controlledRef: (typeof import("@vueuse/core"))["controlledRef"]; + const createApp: (typeof import("vue"))["createApp"]; + const createEventHook: (typeof import("@vueuse/core"))["createEventHook"]; + const createGlobalState: (typeof import("@vueuse/core"))["createGlobalState"]; + const createInjectionState: (typeof import("@vueuse/core"))["createInjectionState"]; + const createPinia: (typeof import("pinia"))["createPinia"]; + const createReactiveFn: (typeof import("@vueuse/core"))["createReactiveFn"]; + const createReusableTemplate: (typeof import("@vueuse/core"))["createReusableTemplate"]; + const createSharedComposable: (typeof import("@vueuse/core"))["createSharedComposable"]; + const createTemplatePromise: (typeof import("@vueuse/core"))["createTemplatePromise"]; + const createUnrefFn: (typeof import("@vueuse/core"))["createUnrefFn"]; + const customRef: (typeof import("vue"))["customRef"]; + const debouncedRef: (typeof import("@vueuse/core"))["debouncedRef"]; + const debouncedWatch: (typeof import("@vueuse/core"))["debouncedWatch"]; + const defineAsyncComponent: (typeof import("vue"))["defineAsyncComponent"]; + const defineComponent: (typeof import("vue"))["defineComponent"]; + const defineStore: (typeof import("pinia"))["defineStore"]; + const eagerComputed: (typeof import("@vueuse/core"))["eagerComputed"]; + const effectScope: (typeof import("vue"))["effectScope"]; + const extendRef: (typeof import("@vueuse/core"))["extendRef"]; + const getActivePinia: (typeof import("pinia"))["getActivePinia"]; + const getCurrentInstance: (typeof import("vue"))["getCurrentInstance"]; + const getCurrentScope: (typeof import("vue"))["getCurrentScope"]; + const h: (typeof import("vue"))["h"]; + const ignorableWatch: (typeof import("@vueuse/core"))["ignorableWatch"]; + const inject: (typeof import("vue"))["inject"]; + const injectLocal: (typeof import("@vueuse/core"))["injectLocal"]; + const isDefined: (typeof import("@vueuse/core"))["isDefined"]; + const isProxy: (typeof import("vue"))["isProxy"]; + const isReactive: (typeof import("vue"))["isReactive"]; + const isReadonly: (typeof import("vue"))["isReadonly"]; + const isRef: (typeof import("vue"))["isRef"]; + const makeDestructurable: (typeof import("@vueuse/core"))["makeDestructurable"]; + const mapActions: (typeof import("pinia"))["mapActions"]; + const mapGetters: (typeof import("pinia"))["mapGetters"]; + const mapState: (typeof import("pinia"))["mapState"]; + const mapStores: (typeof import("pinia"))["mapStores"]; + const mapWritableState: (typeof import("pinia"))["mapWritableState"]; + const markRaw: (typeof import("vue"))["markRaw"]; + const nextTick: (typeof import("vue"))["nextTick"]; + const onActivated: (typeof import("vue"))["onActivated"]; + const onBeforeMount: (typeof import("vue"))["onBeforeMount"]; + const onBeforeRouteLeave: (typeof import("vue-router"))["onBeforeRouteLeave"]; + const onBeforeRouteUpdate: (typeof import("vue-router"))["onBeforeRouteUpdate"]; + const onBeforeUnmount: (typeof import("vue"))["onBeforeUnmount"]; + const onBeforeUpdate: (typeof import("vue"))["onBeforeUpdate"]; + const onClickOutside: (typeof import("@vueuse/core"))["onClickOutside"]; + const onDeactivated: (typeof import("vue"))["onDeactivated"]; + const onErrorCaptured: (typeof import("vue"))["onErrorCaptured"]; + const onKeyStroke: (typeof import("@vueuse/core"))["onKeyStroke"]; + const onLongPress: (typeof import("@vueuse/core"))["onLongPress"]; + const onMounted: (typeof import("vue"))["onMounted"]; + const onRenderTracked: (typeof import("vue"))["onRenderTracked"]; + const onRenderTriggered: (typeof import("vue"))["onRenderTriggered"]; + const onScopeDispose: (typeof import("vue"))["onScopeDispose"]; + const onServerPrefetch: (typeof import("vue"))["onServerPrefetch"]; + const onStartTyping: (typeof import("@vueuse/core"))["onStartTyping"]; + const onUnmounted: (typeof import("vue"))["onUnmounted"]; + const onUpdated: (typeof import("vue"))["onUpdated"]; + const pausableWatch: (typeof import("@vueuse/core"))["pausableWatch"]; + const provide: (typeof import("vue"))["provide"]; + const provideLocal: (typeof import("@vueuse/core"))["provideLocal"]; + const reactify: (typeof import("@vueuse/core"))["reactify"]; + const reactifyObject: (typeof import("@vueuse/core"))["reactifyObject"]; + const reactive: (typeof import("vue"))["reactive"]; + const reactiveComputed: (typeof import("@vueuse/core"))["reactiveComputed"]; + const reactiveOmit: (typeof import("@vueuse/core"))["reactiveOmit"]; + const reactivePick: (typeof import("@vueuse/core"))["reactivePick"]; + const readonly: (typeof import("vue"))["readonly"]; + const ref: (typeof import("vue"))["ref"]; + const refAutoReset: (typeof import("@vueuse/core"))["refAutoReset"]; + const refDebounced: (typeof import("@vueuse/core"))["refDebounced"]; + const refDefault: (typeof import("@vueuse/core"))["refDefault"]; + const refThrottled: (typeof import("@vueuse/core"))["refThrottled"]; + const refWithControl: (typeof import("@vueuse/core"))["refWithControl"]; + const resolveComponent: (typeof import("vue"))["resolveComponent"]; + const resolveRef: (typeof import("@vueuse/core"))["resolveRef"]; + const resolveUnref: (typeof import("@vueuse/core"))["resolveUnref"]; + const setActivePinia: (typeof import("pinia"))["setActivePinia"]; + const setMapStoreSuffix: (typeof import("pinia"))["setMapStoreSuffix"]; + const shallowReactive: (typeof import("vue"))["shallowReactive"]; + const shallowReadonly: (typeof import("vue"))["shallowReadonly"]; + const shallowRef: (typeof import("vue"))["shallowRef"]; + const storeToRefs: (typeof import("pinia"))["storeToRefs"]; + const syncRef: (typeof import("@vueuse/core"))["syncRef"]; + const syncRefs: (typeof import("@vueuse/core"))["syncRefs"]; + const templateRef: (typeof import("@vueuse/core"))["templateRef"]; + const throttledRef: (typeof import("@vueuse/core"))["throttledRef"]; + const throttledWatch: (typeof import("@vueuse/core"))["throttledWatch"]; + const toRaw: (typeof import("vue"))["toRaw"]; + const toReactive: (typeof import("@vueuse/core"))["toReactive"]; + const toRef: (typeof import("vue"))["toRef"]; + const toRefs: (typeof import("vue"))["toRefs"]; + const toValue: (typeof import("vue"))["toValue"]; + const triggerRef: (typeof import("vue"))["triggerRef"]; + const tryOnBeforeMount: (typeof import("@vueuse/core"))["tryOnBeforeMount"]; + const tryOnBeforeUnmount: (typeof import("@vueuse/core"))["tryOnBeforeUnmount"]; + const tryOnMounted: (typeof import("@vueuse/core"))["tryOnMounted"]; + const tryOnScopeDispose: (typeof import("@vueuse/core"))["tryOnScopeDispose"]; + const tryOnUnmounted: (typeof import("@vueuse/core"))["tryOnUnmounted"]; + const unref: (typeof import("vue"))["unref"]; + const unrefElement: (typeof import("@vueuse/core"))["unrefElement"]; + const until: (typeof import("@vueuse/core"))["until"]; + const useActiveElement: (typeof import("@vueuse/core"))["useActiveElement"]; + const useAnimate: (typeof import("@vueuse/core"))["useAnimate"]; + const useArrayDifference: (typeof import("@vueuse/core"))["useArrayDifference"]; + const useArrayEvery: (typeof import("@vueuse/core"))["useArrayEvery"]; + const useArrayFilter: (typeof import("@vueuse/core"))["useArrayFilter"]; + const useArrayFind: (typeof import("@vueuse/core"))["useArrayFind"]; + const useArrayFindIndex: (typeof import("@vueuse/core"))["useArrayFindIndex"]; + const useArrayFindLast: (typeof import("@vueuse/core"))["useArrayFindLast"]; + const useArrayIncludes: (typeof import("@vueuse/core"))["useArrayIncludes"]; + const useArrayJoin: (typeof import("@vueuse/core"))["useArrayJoin"]; + const useArrayMap: (typeof import("@vueuse/core"))["useArrayMap"]; + const useArrayReduce: (typeof import("@vueuse/core"))["useArrayReduce"]; + const useArraySome: (typeof import("@vueuse/core"))["useArraySome"]; + const useArrayUnique: (typeof import("@vueuse/core"))["useArrayUnique"]; + const useAsyncQueue: (typeof import("@vueuse/core"))["useAsyncQueue"]; + const useAsyncState: (typeof import("@vueuse/core"))["useAsyncState"]; + const useAttrs: (typeof import("vue"))["useAttrs"]; + const useBase64: (typeof import("@vueuse/core"))["useBase64"]; + const useBattery: (typeof import("@vueuse/core"))["useBattery"]; + const useBluetooth: (typeof import("@vueuse/core"))["useBluetooth"]; + const useBreakpoints: (typeof import("@vueuse/core"))["useBreakpoints"]; + const useBroadcastChannel: (typeof import("@vueuse/core"))["useBroadcastChannel"]; + const useBrowserLocation: (typeof import("@vueuse/core"))["useBrowserLocation"]; + const useCached: (typeof import("@vueuse/core"))["useCached"]; + const useClipboard: (typeof import("@vueuse/core"))["useClipboard"]; + const useClipboardItems: (typeof import("@vueuse/core"))["useClipboardItems"]; + const useCloned: (typeof import("@vueuse/core"))["useCloned"]; + const useColorMode: (typeof import("@vueuse/core"))["useColorMode"]; + const useConfirmDialog: (typeof import("@vueuse/core"))["useConfirmDialog"]; + const useCounter: (typeof import("@vueuse/core"))["useCounter"]; + const useCssModule: (typeof import("vue"))["useCssModule"]; + const useCssVar: (typeof import("@vueuse/core"))["useCssVar"]; + const useCssVars: (typeof import("vue"))["useCssVars"]; + const useCurrentElement: (typeof import("@vueuse/core"))["useCurrentElement"]; + const useCycleList: (typeof import("@vueuse/core"))["useCycleList"]; + const useDark: (typeof import("@vueuse/core"))["useDark"]; + const useDateFormat: (typeof import("@vueuse/core"))["useDateFormat"]; + const useDebounce: (typeof import("@vueuse/core"))["useDebounce"]; + const useDebounceFn: (typeof import("@vueuse/core"))["useDebounceFn"]; + const useDebouncedRefHistory: (typeof import("@vueuse/core"))["useDebouncedRefHistory"]; + const useDeviceMotion: (typeof import("@vueuse/core"))["useDeviceMotion"]; + const useDeviceOrientation: (typeof import("@vueuse/core"))["useDeviceOrientation"]; + const useDevicePixelRatio: (typeof import("@vueuse/core"))["useDevicePixelRatio"]; + const useDevicesList: (typeof import("@vueuse/core"))["useDevicesList"]; + const useDisplayMedia: (typeof import("@vueuse/core"))["useDisplayMedia"]; + const useDocumentVisibility: (typeof import("@vueuse/core"))["useDocumentVisibility"]; + const useDraggable: (typeof import("@vueuse/core"))["useDraggable"]; + const useDropZone: (typeof import("@vueuse/core"))["useDropZone"]; + const useElementBounding: (typeof import("@vueuse/core"))["useElementBounding"]; + const useElementByPoint: (typeof import("@vueuse/core"))["useElementByPoint"]; + const useElementHover: (typeof import("@vueuse/core"))["useElementHover"]; + const useElementSize: (typeof import("@vueuse/core"))["useElementSize"]; + const useElementVisibility: (typeof import("@vueuse/core"))["useElementVisibility"]; + const useEventBus: (typeof import("@vueuse/core"))["useEventBus"]; + const useEventListener: (typeof import("@vueuse/core"))["useEventListener"]; + const useEventSource: (typeof import("@vueuse/core"))["useEventSource"]; + const useEyeDropper: (typeof import("@vueuse/core"))["useEyeDropper"]; + const useFavicon: (typeof import("@vueuse/core"))["useFavicon"]; + const useFetch: (typeof import("@vueuse/core"))["useFetch"]; + const useFileDialog: (typeof import("@vueuse/core"))["useFileDialog"]; + const useFileSystemAccess: (typeof import("@vueuse/core"))["useFileSystemAccess"]; + const useFocus: (typeof import("@vueuse/core"))["useFocus"]; + const useFocusWithin: (typeof import("@vueuse/core"))["useFocusWithin"]; + const useFps: (typeof import("@vueuse/core"))["useFps"]; + const useFullscreen: (typeof import("@vueuse/core"))["useFullscreen"]; + const useGamepad: (typeof import("@vueuse/core"))["useGamepad"]; + const useGeolocation: (typeof import("@vueuse/core"))["useGeolocation"]; + const useIdle: (typeof import("@vueuse/core"))["useIdle"]; + const useImage: (typeof import("@vueuse/core"))["useImage"]; + const useInfiniteScroll: (typeof import("@vueuse/core"))["useInfiniteScroll"]; + const useIntersectionObserver: (typeof import("@vueuse/core"))["useIntersectionObserver"]; + const useInterval: (typeof import("@vueuse/core"))["useInterval"]; + const useIntervalFn: (typeof import("@vueuse/core"))["useIntervalFn"]; + const useKeyModifier: (typeof import("@vueuse/core"))["useKeyModifier"]; + const useLastChanged: (typeof import("@vueuse/core"))["useLastChanged"]; + const useLink: (typeof import("vue-router"))["useLink"]; + const useLocalStorage: (typeof import("@vueuse/core"))["useLocalStorage"]; + const useMagicKeys: (typeof import("@vueuse/core"))["useMagicKeys"]; + const useManualRefHistory: (typeof import("@vueuse/core"))["useManualRefHistory"]; + const useMediaControls: (typeof import("@vueuse/core"))["useMediaControls"]; + const useMediaQuery: (typeof import("@vueuse/core"))["useMediaQuery"]; + const useMemoize: (typeof import("@vueuse/core"))["useMemoize"]; + const useMemory: (typeof import("@vueuse/core"))["useMemory"]; + const useMounted: (typeof import("@vueuse/core"))["useMounted"]; + const useMouse: (typeof import("@vueuse/core"))["useMouse"]; + const useMouseInElement: (typeof import("@vueuse/core"))["useMouseInElement"]; + const useMousePressed: (typeof import("@vueuse/core"))["useMousePressed"]; + const useMutationObserver: (typeof import("@vueuse/core"))["useMutationObserver"]; + const useNavigatorLanguage: (typeof import("@vueuse/core"))["useNavigatorLanguage"]; + const useNetwork: (typeof import("@vueuse/core"))["useNetwork"]; + const useNow: (typeof import("@vueuse/core"))["useNow"]; + const useObjectUrl: (typeof import("@vueuse/core"))["useObjectUrl"]; + const useOffsetPagination: (typeof import("@vueuse/core"))["useOffsetPagination"]; + const useOnline: (typeof import("@vueuse/core"))["useOnline"]; + const usePageLeave: (typeof import("@vueuse/core"))["usePageLeave"]; + const useParallax: (typeof import("@vueuse/core"))["useParallax"]; + const useParentElement: (typeof import("@vueuse/core"))["useParentElement"]; + const usePerformanceObserver: (typeof import("@vueuse/core"))["usePerformanceObserver"]; + const usePermission: (typeof import("@vueuse/core"))["usePermission"]; + const usePointer: (typeof import("@vueuse/core"))["usePointer"]; + const usePointerLock: (typeof import("@vueuse/core"))["usePointerLock"]; + const usePointerSwipe: (typeof import("@vueuse/core"))["usePointerSwipe"]; + const usePreferredColorScheme: (typeof import("@vueuse/core"))["usePreferredColorScheme"]; + const usePreferredContrast: (typeof import("@vueuse/core"))["usePreferredContrast"]; + const usePreferredDark: (typeof import("@vueuse/core"))["usePreferredDark"]; + const usePreferredLanguages: (typeof import("@vueuse/core"))["usePreferredLanguages"]; + const usePreferredReducedMotion: (typeof import("@vueuse/core"))["usePreferredReducedMotion"]; + const usePrevious: (typeof import("@vueuse/core"))["usePrevious"]; + const useRafFn: (typeof import("@vueuse/core"))["useRafFn"]; + const useRefHistory: (typeof import("@vueuse/core"))["useRefHistory"]; + const useResizeObserver: (typeof import("@vueuse/core"))["useResizeObserver"]; + const useRoute: (typeof import("vue-router"))["useRoute"]; + const useRouter: (typeof import("vue-router"))["useRouter"]; + const useScreenOrientation: (typeof import("@vueuse/core"))["useScreenOrientation"]; + const useScreenSafeArea: (typeof import("@vueuse/core"))["useScreenSafeArea"]; + const useScriptTag: (typeof import("@vueuse/core"))["useScriptTag"]; + const useScroll: (typeof import("@vueuse/core"))["useScroll"]; + const useScrollLock: (typeof import("@vueuse/core"))["useScrollLock"]; + const useSessionStorage: (typeof import("@vueuse/core"))["useSessionStorage"]; + const useShare: (typeof import("@vueuse/core"))["useShare"]; + const useSlots: (typeof import("vue"))["useSlots"]; + const useSorted: (typeof import("@vueuse/core"))["useSorted"]; + const useSpeechRecognition: (typeof import("@vueuse/core"))["useSpeechRecognition"]; + const useSpeechSynthesis: (typeof import("@vueuse/core"))["useSpeechSynthesis"]; + const useStepper: (typeof import("@vueuse/core"))["useStepper"]; + const useStorage: (typeof import("@vueuse/core"))["useStorage"]; + const useStorageAsync: (typeof import("@vueuse/core"))["useStorageAsync"]; + const useStyleTag: (typeof import("@vueuse/core"))["useStyleTag"]; + const useSupported: (typeof import("@vueuse/core"))["useSupported"]; + const useSwipe: (typeof import("@vueuse/core"))["useSwipe"]; + const useTemplateRefsList: (typeof import("@vueuse/core"))["useTemplateRefsList"]; + const useTextDirection: (typeof import("@vueuse/core"))["useTextDirection"]; + const useTextSelection: (typeof import("@vueuse/core"))["useTextSelection"]; + const useTextareaAutosize: (typeof import("@vueuse/core"))["useTextareaAutosize"]; + const useThrottle: (typeof import("@vueuse/core"))["useThrottle"]; + const useThrottleFn: (typeof import("@vueuse/core"))["useThrottleFn"]; + const useThrottledRefHistory: (typeof import("@vueuse/core"))["useThrottledRefHistory"]; + const useTimeAgo: (typeof import("@vueuse/core"))["useTimeAgo"]; + const useTimeout: (typeof import("@vueuse/core"))["useTimeout"]; + const useTimeoutFn: (typeof import("@vueuse/core"))["useTimeoutFn"]; + const useTimeoutPoll: (typeof import("@vueuse/core"))["useTimeoutPoll"]; + const useTimestamp: (typeof import("@vueuse/core"))["useTimestamp"]; + const useTitle: (typeof import("@vueuse/core"))["useTitle"]; + const useToNumber: (typeof import("@vueuse/core"))["useToNumber"]; + const useToString: (typeof import("@vueuse/core"))["useToString"]; + const useToggle: (typeof import("@vueuse/core"))["useToggle"]; + const useTransition: (typeof import("@vueuse/core"))["useTransition"]; + const useUrlSearchParams: (typeof import("@vueuse/core"))["useUrlSearchParams"]; + const useUserMedia: (typeof import("@vueuse/core"))["useUserMedia"]; + const useVModel: (typeof import("@vueuse/core"))["useVModel"]; + const useVModels: (typeof import("@vueuse/core"))["useVModels"]; + const useVibrate: (typeof import("@vueuse/core"))["useVibrate"]; + const useVirtualList: (typeof import("@vueuse/core"))["useVirtualList"]; + const useWakeLock: (typeof import("@vueuse/core"))["useWakeLock"]; + const useWebNotification: (typeof import("@vueuse/core"))["useWebNotification"]; + const useWebSocket: (typeof import("@vueuse/core"))["useWebSocket"]; + const useWebWorker: (typeof import("@vueuse/core"))["useWebWorker"]; + const useWebWorkerFn: (typeof import("@vueuse/core"))["useWebWorkerFn"]; + const useWindowFocus: (typeof import("@vueuse/core"))["useWindowFocus"]; + const useWindowScroll: (typeof import("@vueuse/core"))["useWindowScroll"]; + const useWindowSize: (typeof import("@vueuse/core"))["useWindowSize"]; + const watch: (typeof import("vue"))["watch"]; + const watchArray: (typeof import("@vueuse/core"))["watchArray"]; + const watchAtMost: (typeof import("@vueuse/core"))["watchAtMost"]; + const watchDebounced: (typeof import("@vueuse/core"))["watchDebounced"]; + const watchDeep: (typeof import("@vueuse/core"))["watchDeep"]; + const watchEffect: (typeof import("vue"))["watchEffect"]; + const watchIgnorable: (typeof import("@vueuse/core"))["watchIgnorable"]; + const watchImmediate: (typeof import("@vueuse/core"))["watchImmediate"]; + const watchOnce: (typeof import("@vueuse/core"))["watchOnce"]; + const watchPausable: (typeof import("@vueuse/core"))["watchPausable"]; + const watchPostEffect: (typeof import("vue"))["watchPostEffect"]; + const watchSyncEffect: (typeof import("vue"))["watchSyncEffect"]; + const watchThrottled: (typeof import("@vueuse/core"))["watchThrottled"]; + const watchTriggerable: (typeof import("@vueuse/core"))["watchTriggerable"]; + const watchWithFilter: (typeof import("@vueuse/core"))["watchWithFilter"]; + const whenever: (typeof import("@vueuse/core"))["whenever"]; +} +// for type re-export +declare global { + // @ts-ignore + export type { + Component, + ComponentPublicInstance, + ComputedRef, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + VNode, + WritableComputedRef, + } from "vue"; + import("vue"); +} +// for vue template auto import +import { UnwrapRef } from "vue"; +declare module "vue" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>; + readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>; + readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>; + readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>; + readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>; + readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>; + readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>; + readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>; + readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>; + readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>; + readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>; + readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>; + readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>; + readonly createInjectionState: UnwrapRef< + (typeof import("@vueuse/core"))["createInjectionState"] + >; + readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>; + readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>; + readonly createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>; + readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>; + readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>; + readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>; + readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>; + readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>; + readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>; + readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>; + readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>; + readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>; + readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>; + readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>; + readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>; + readonly h: UnwrapRef<(typeof import("vue"))["h"]>; + readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>; + readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>; + readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>; + readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>; + readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>; + readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>; + readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>; + readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>; + readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>; + readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>; + readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>; + readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>; + readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>; + readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>; + readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>; + readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>; + readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>; + readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>; + readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>; + readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>; + readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>; + readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>; + readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>; + readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>; + readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>; + readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>; + readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>; + readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>; + readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>; + readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>; + readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>; + readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>; + readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>; + readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>; + readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>; + readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>; + readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>; + readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>; + readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>; + readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>; + readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>; + readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>; + readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>; + readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>; + readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>; + readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>; + readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>; + readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>; + readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>; + readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>; + readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>; + readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>; + readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>; + readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>; + readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>; + readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>; + readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>; + readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>; + readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>; + readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>; + readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>; + readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>; + readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>; + readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>; + readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>; + readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>; + readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>; + readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>; + readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>; + readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>; + readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>; + readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>; + readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>; + readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>; + readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>; + readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>; + readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>; + readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>; + readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>; + readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>; + readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>; + readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>; + readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>; + readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>; + readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>; + readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>; + readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>; + readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>; + readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>; + readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>; + readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>; + readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>; + readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>; + readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>; + readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>; + readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>; + readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>; + readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>; + readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>; + readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>; + readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>; + readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>; + readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>; + readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>; + readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>; + readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>; + readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>; + readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>; + readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>; + readonly useDebouncedRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useDebouncedRefHistory"] + >; + readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>; + readonly useDeviceOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useDeviceOrientation"] + >; + readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>; + readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>; + readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>; + readonly useDocumentVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useDocumentVisibility"] + >; + readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>; + readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>; + readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>; + readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>; + readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>; + readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>; + readonly useElementVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useElementVisibility"] + >; + readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>; + readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>; + readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>; + readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>; + readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>; + readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>; + readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>; + readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>; + readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>; + readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>; + readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>; + readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>; + readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>; + readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>; + readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>; + readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>; + readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>; + readonly useIntersectionObserver: UnwrapRef< + (typeof import("@vueuse/core"))["useIntersectionObserver"] + >; + readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>; + readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>; + readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>; + readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>; + readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>; + readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>; + readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>; + readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>; + readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>; + readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>; + readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>; + readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>; + readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>; + readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>; + readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>; + readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>; + readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>; + readonly useNavigatorLanguage: UnwrapRef< + (typeof import("@vueuse/core"))["useNavigatorLanguage"] + >; + readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>; + readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>; + readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>; + readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>; + readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>; + readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>; + readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>; + readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>; + readonly usePreferredColorScheme: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredColorScheme"] + >; + readonly usePreferredContrast: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredContrast"] + >; + readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>; + readonly usePreferredLanguages: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredLanguages"] + >; + readonly usePreferredReducedMotion: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredReducedMotion"] + >; + readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>; + readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>; + readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>; + readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>; + readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>; + readonly useScreenOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useScreenOrientation"] + >; + readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>; + readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>; + readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>; + readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>; + readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>; + readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>; + readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>; + readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>; + readonly useSpeechRecognition: UnwrapRef< + (typeof import("@vueuse/core"))["useSpeechRecognition"] + >; + readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>; + readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>; + readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>; + readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>; + readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>; + readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>; + readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>; + readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>; + readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>; + readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>; + readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>; + readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>; + readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>; + readonly useThrottledRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useThrottledRefHistory"] + >; + readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>; + readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>; + readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>; + readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>; + readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>; + readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>; + readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>; + readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>; + readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>; + readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>; + readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>; + readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>; + readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>; + readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>; + readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>; + readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>; + readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>; + readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>; + readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>; + readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>; + readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>; + readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>; + readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>; + readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>; + readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>; + readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>; + readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>; + readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>; + readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>; + readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>; + readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>; + readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>; + readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>; + readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>; + readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>; + readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>; + } +} +declare module "@vue/runtime-core" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>; + readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>; + readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>; + readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>; + readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>; + readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>; + readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>; + readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>; + readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>; + readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>; + readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>; + readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>; + readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>; + readonly createInjectionState: UnwrapRef< + (typeof import("@vueuse/core"))["createInjectionState"] + >; + readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>; + readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>; + readonly createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>; + readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>; + readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>; + readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>; + readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>; + readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>; + readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>; + readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>; + readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>; + readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>; + readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>; + readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>; + readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>; + readonly h: UnwrapRef<(typeof import("vue"))["h"]>; + readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>; + readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>; + readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>; + readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>; + readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>; + readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>; + readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>; + readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>; + readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>; + readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>; + readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>; + readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>; + readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>; + readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>; + readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>; + readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>; + readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>; + readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>; + readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>; + readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>; + readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>; + readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>; + readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>; + readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>; + readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>; + readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>; + readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>; + readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>; + readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>; + readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>; + readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>; + readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>; + readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>; + readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>; + readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>; + readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>; + readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>; + readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>; + readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>; + readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>; + readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>; + readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>; + readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>; + readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>; + readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>; + readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>; + readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>; + readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>; + readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>; + readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>; + readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>; + readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>; + readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>; + readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>; + readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>; + readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>; + readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>; + readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>; + readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>; + readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>; + readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>; + readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>; + readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>; + readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>; + readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>; + readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>; + readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>; + readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>; + readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>; + readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>; + readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>; + readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>; + readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>; + readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>; + readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>; + readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>; + readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>; + readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>; + readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>; + readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>; + readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>; + readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>; + readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>; + readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>; + readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>; + readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>; + readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>; + readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>; + readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>; + readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>; + readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>; + readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>; + readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>; + readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>; + readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>; + readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>; + readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>; + readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>; + readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>; + readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>; + readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>; + readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>; + readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>; + readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>; + readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>; + readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>; + readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>; + readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>; + readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>; + readonly useDebouncedRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useDebouncedRefHistory"] + >; + readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>; + readonly useDeviceOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useDeviceOrientation"] + >; + readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>; + readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>; + readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>; + readonly useDocumentVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useDocumentVisibility"] + >; + readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>; + readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>; + readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>; + readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>; + readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>; + readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>; + readonly useElementVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useElementVisibility"] + >; + readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>; + readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>; + readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>; + readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>; + readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>; + readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>; + readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>; + readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>; + readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>; + readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>; + readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>; + readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>; + readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>; + readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>; + readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>; + readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>; + readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>; + readonly useIntersectionObserver: UnwrapRef< + (typeof import("@vueuse/core"))["useIntersectionObserver"] + >; + readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>; + readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>; + readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>; + readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>; + readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>; + readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>; + readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>; + readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>; + readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>; + readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>; + readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>; + readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>; + readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>; + readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>; + readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>; + readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>; + readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>; + readonly useNavigatorLanguage: UnwrapRef< + (typeof import("@vueuse/core"))["useNavigatorLanguage"] + >; + readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>; + readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>; + readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>; + readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>; + readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>; + readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>; + readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>; + readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>; + readonly usePreferredColorScheme: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredColorScheme"] + >; + readonly usePreferredContrast: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredContrast"] + >; + readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>; + readonly usePreferredLanguages: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredLanguages"] + >; + readonly usePreferredReducedMotion: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredReducedMotion"] + >; + readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>; + readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>; + readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>; + readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>; + readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>; + readonly useScreenOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useScreenOrientation"] + >; + readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>; + readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>; + readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>; + readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>; + readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>; + readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>; + readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>; + readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>; + readonly useSpeechRecognition: UnwrapRef< + (typeof import("@vueuse/core"))["useSpeechRecognition"] + >; + readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>; + readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>; + readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>; + readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>; + readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>; + readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>; + readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>; + readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>; + readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>; + readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>; + readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>; + readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>; + readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>; + readonly useThrottledRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useThrottledRefHistory"] + >; + readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>; + readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>; + readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>; + readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>; + readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>; + readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>; + readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>; + readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>; + readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>; + readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>; + readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>; + readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>; + readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>; + readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>; + readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>; + readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>; + readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>; + readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>; + readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>; + readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>; + readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>; + readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>; + readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>; + readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>; + readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>; + readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>; + readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>; + readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>; + readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>; + readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>; + readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>; + readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>; + readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>; + readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>; + readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>; + readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>; + } +} diff --git a/src/types/components.d.ts b/src/types/components.d.ts new file mode 100644 index 0000000..daf63fa --- /dev/null +++ b/src/types/components.d.ts @@ -0,0 +1,97 @@ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module "vue" { + export interface GlobalComponents { + AppLink: (typeof import("./../components/AppLink/index.vue"))["default"]; + AppMain: (typeof import("./../layout/components/AppMain/index.vue"))["default"]; + Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"]; + CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"]; + CURD: (typeof import("./../components/CURD/index.vue"))["default"]; + Dict: (typeof import("./../components/Dict/index.vue"))["default"]; + DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"]; + ElBacktop: (typeof import("element-plus/es"))["ElBacktop"]; + ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"]; + ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"]; + ElButton: (typeof import("element-plus/es"))["ElButton"]; + ElCard: (typeof import("element-plus/es"))["ElCard"]; + ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"]; + ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"]; + ElCol: (typeof import("element-plus/es"))["ElCol"]; + ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"]; + ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"]; + ElDatePicker: (typeof import("element-plus/es"))["ElDatePicker"]; + ElDialog: (typeof import("element-plus/es"))["ElDialog"]; + ElDivider: (typeof import("element-plus/es"))["ElDivider"]; + ElDrawer: (typeof import("element-plus/es"))["ElDrawer"]; + ElDropdown: (typeof import("element-plus/es"))["ElDropdown"]; + ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"]; + ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"]; + ElForm: (typeof import("element-plus/es"))["ElForm"]; + ElFormItem: (typeof import("element-plus/es"))["ElFormItem"]; + ElIcon: (typeof import("element-plus/es"))["ElIcon"]; + ElImage: (typeof import("element-plus/es"))["ElImage"]; + ElInput: (typeof import("element-plus/es"))["ElInput"]; + ElInputNumber: (typeof import("element-plus/es"))["ElInputNumber"]; + ElLink: (typeof import("element-plus/es"))["ElLink"]; + ElMenu: (typeof import("element-plus/es"))["ElMenu"]; + ElMenuItem: (typeof import("element-plus/es"))["ElMenuItem"]; + ElOption: (typeof import("element-plus/es"))["ElOption"]; + ElPagination: (typeof import("element-plus/es"))["ElPagination"]; + ElPopover: (typeof import("element-plus/es"))["ElPopover"]; + ElRadio: (typeof import("element-plus/es"))["ElRadio"]; + ElRadioGroup: (typeof import("element-plus/es"))["ElRadioGroup"]; + ElRow: (typeof import("element-plus/es"))["ElRow"]; + ElScrollbar: (typeof import("element-plus/es"))["ElScrollbar"]; + ElSelect: (typeof import("element-plus/es"))["ElSelect"]; + ElStatistic: (typeof import("element-plus/es"))["ElStatistic"]; + ElSubMenu: (typeof import("element-plus/es"))["ElSubMenu"]; + ElSwitch: (typeof import("element-plus/es"))["ElSwitch"]; + ElTable: (typeof import("element-plus/es"))["ElTable"]; + ElTableColumn: (typeof import("element-plus/es"))["ElTableColumn"]; + ElTag: (typeof import("element-plus/es"))["ElTag"]; + ElText: (typeof import("element-plus/es"))["ElText"]; + ElTooltip: (typeof import("element-plus/es"))["ElTooltip"]; + ElTree: (typeof import("element-plus/es"))["ElTree"]; + ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"]; + ElUpload: (typeof import("element-plus/es"))["ElUpload"]; + ElWatermark: (typeof import("element-plus/es"))["ElWatermark"]; + ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"]; + FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"]; + Form: (typeof import("./../components/CURD/Form.vue"))["default"]; + Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"]; + GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"]; + Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"]; + IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"]; + MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"]; + LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"]; + PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"]; + PageForm: (typeof import("./../components/CURD/PageForm.vue"))["default"]; + PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"]; + PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"]; + Pagination: (typeof import("./../components/Pagination/index.vue"))["default"]; + RouterLink: (typeof import("vue-router"))["RouterLink"]; + RouterView: (typeof import("vue-router"))["RouterView"]; + Settings: (typeof import("./../layout/components/Settings/index.vue"))["default"]; + Sidebar: (typeof import("./../layout/components/Sidebar/index.vue"))["default"]; + SidebarLogo: (typeof import("./../layout/components/Sidebar/components/SidebarLogo.vue"))["default"]; + SidebarMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMenu.vue"))["default"]; + SidebarMenuItem: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItem.vue"))["default"]; + SidebarMenuItemTitle: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItemTitle.vue"))["default"]; + SidebarMixTopMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue"))["default"]; + SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"]; + SvgIcon: (typeof import("./../components/SvgIcon/index.vue"))["default"]; + TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"]; + TagsView: (typeof import("./../layout/components/TagsView/index.vue"))["default"]; + ThemeColorPicker: (typeof import("./../layout/components/Settings/components/ThemeColorPicker.vue"))["default"]; + SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"]; + WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"]; + ImgCorpper: (typeof import("./../components/ImgCorpper/index.vue"))["default"]; + } + export interface ComponentCustomProperties { + vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"]; + } +} diff --git a/src/types/env.d.ts b/src/types/env.d.ts new file mode 100644 index 0000000..9ecf2d5 --- /dev/null +++ b/src/types/env.d.ts @@ -0,0 +1,33 @@ +// https://cn.vitejs.dev/guide/env-and-mode + +// TypeScript 绫诲瀷鎻愮ず閮戒负 string锛� https://github.com/vitejs/vite/issues/6930 +interface ImportMetaEnv { + /** 搴旂敤绔彛 */ + VITE_APP_PORT: number; + /** API 鍩虹璺緞(浠g悊鍓嶇紑) */ + VITE_APP_BASE_API: string; + /** API 鍦板潃 */ + VITE_APP_API_URL: string; + /** 鏄惁寮�鍚� Mock 鏈嶅姟 */ + VITE_MOCK_DEV_SERVER: boolean; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +/** + * 骞冲彴鐨勫悕绉般�佺増鏈�佽繍琛屾墍闇�鐨刵ode鐗堟湰銆佷緷璧栥�佹瀯寤烘椂闂寸殑绫诲瀷鎻愮ず + */ +declare const __APP_INFO__: { + pkg: { + name: string; + version: string; + engines: { + node: string; + }; + dependencies: Record<string, string>; + devDependencies: Record<string, string>; + }; + buildTimestamp: number; +}; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..fdc89b4 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,107 @@ +declare global { + /** + * 鍝嶅簲鏁版嵁 + */ + interface ResponseData<T = any> { + code: string; + data: T; + msg: string; + } + + /** + * 鍒嗛〉鏌ヨ鍙傛暟 + */ + interface PageQuery { + pageNum: number; + pageSize: number; + } + + /** + * 鍒嗛〉鍝嶅簲瀵硅薄 + */ + interface PageResult<T> { + /** 鏁版嵁鍒楄〃 */ + list: T; + /** 鎬绘暟 */ + total: number; + } + + /** + * 椤电瀵硅薄 + */ + interface TagView { + /** 椤电鍚嶇О */ + name: string; + /** 椤电鏍囬 */ + title: string; + /** 椤电璺敱璺緞 */ + path: string; + /** 椤电璺敱瀹屾暣璺緞 */ + fullPath: string; + /** 椤电鍥炬爣 */ + icon?: string; + /** 鏄惁鍥哄畾椤电 */ + affix?: boolean; + /** 鏄惁寮�鍚紦瀛� */ + keepAlive?: boolean; + /** 璺敱鏌ヨ鍙傛暟 */ + query?: any; + } + + /** + * 绯荤粺璁剧疆 + */ + interface AppSettings { + /** 绯荤粺鏍囬 */ + title: string; + /** 绯荤粺鐗堟湰 */ + version: string; + /** 鏄惁鏄剧ず璁剧疆 */ + showSettings: boolean; + /** 鏄惁鏄剧ず澶氭爣绛惧鑸� */ + tagsView: boolean; + /** 鏄惁鏄剧ず渚ц竟鏍廘ogo */ + sidebarLogo: boolean; + /** 瀵艰埅鏍忓竷灞�(left|top|mix) */ + layout: string; + /** 涓婚棰滆壊 */ + themeColor: string; + /** 涓婚妯″紡(dark|light) */ + theme: string; + /** 甯冨眬澶у皬(default |large |small) */ + size: string; + /** 璇█( zh-cn| en) */ + language: string; + /** 鏄惁寮�鍚按鍗� */ + watermarkEnabled: boolean; + /** 姘村嵃鍐呭 */ + watermarkContent: string; + } + + /** + * 涓嬫媺閫夐」鏁版嵁绫诲瀷 + */ + interface OptionType { + /** 鍊� */ + value: string | number; + /** 鏂囨湰 */ + label: string; + /** 瀛愬垪琛� */ + children?: OptionType[]; + } + + /** + * 瀵煎叆缁撴灉 + */ + interface ExcelResult { + /** 鐘舵�佺爜 */ + code: string; + /** 鏃犳晥鏁版嵁鏉℃暟 */ + invalidCount: number; + /** 鏈夋晥鏁版嵁鏉℃暟 */ + validCount: number; + /** 閿欒淇℃伅 */ + messageList: Array<string>; + } +} +export {}; diff --git a/src/types/request-result.ts b/src/types/request-result.ts new file mode 100644 index 0000000..2a56d89 --- /dev/null +++ b/src/types/request-result.ts @@ -0,0 +1,58 @@ +export interface ResultV0<T> { + /** 鎺ュ彛鏄惁璋冪敤鎴愬姛 */ + code: number; + + /** 鏄惁鐧诲綍鎴愬姛 */ + data: boolean; + + /** 杩斿洖缁撴灉 */ + data2?: T; + + /** 杩斿洖鎻愮ず */ + msg: string; +} + +/** + * 涓ゅ眰code甯﹀垎椤靛拰list缁撴瀯鐨勬煡璇㈢粨鏋� + */ +export interface ResultV1<T> { + code: number; + data: { + code: number; + data: { + total: number; + list: T[]; + }; + pageNum: number; + pageSize: number; + msg: string; + }; +} + +/** + * 1灞俢ode甯﹀垎椤靛拰list缁撴瀯鐨勬煡璇㈢粨鏋� + */ +export interface ResultV2<T> { + code: number; + data: { + total: number; + list: T[]; + }; + pageNum: number; + pageSize: number; + msg: string; +} + +/** + * 鏃犲垎椤垫煡璇㈢粨鏋� + */ +export interface ResultV3<T> { + code: number; + data: T[]; + msg: string; +} + +export interface ResultV4 { + code: number; + msg: string; +} diff --git a/src/types/router.d.ts b/src/types/router.d.ts new file mode 100644 index 0000000..17a3cb4 --- /dev/null +++ b/src/types/router.d.ts @@ -0,0 +1,54 @@ +import "vue-router"; + +declare module "vue-router" { + // https://router.vuejs.org/zh/guide/advanced/meta.html#typescript + // 鍙互閫氳繃鎵╁睍 RouteMeta 鎺ュ彛鏉ヨ緭鍏� meta 瀛楁 + interface RouteMeta { + /** + * 鑿滃崟鍚嶇О + * @example 'Dashboard' + */ + title?: string; + + /** + * 鑿滃崟鍥炬爣 + * @example 'el-icon-edit' + */ + icon?: string; + + /** + * 鏄惁闅愯棌鑿滃崟 + * true 闅愯棌, false 鏄剧ず + * @default false + */ + hidden?: boolean; + + /** + * 濮嬬粓鏄剧ず鐖剁骇鑿滃崟锛屽嵆浣垮彧鏈変竴涓瓙鑿滃崟 + * true 鏄剧ず鐖剁骇鑿滃崟, false 闅愯棌鐖剁骇鑿滃崟锛屾樉绀哄敮涓�瀛愯妭鐐� + * @default false + */ + alwaysShow?: boolean; + + /** + * 鏄惁鍥哄畾鍦ㄩ〉绛句笂 + * true 鍥哄畾, false 涓嶅浐瀹� + * @default false + */ + affix?: boolean; + + /** + * 鏄惁缂撳瓨椤甸潰 + * true 缂撳瓨, false 涓嶇紦瀛� + * @default false + */ + keepAlive?: boolean; + + /** + * 鏄惁鍦ㄩ潰鍖呭睉瀵艰埅涓殣钘� + * true 闅愯棌, false 鏄剧ず + * @default false + */ + breadcrumb?: boolean; + } +} diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts new file mode 100644 index 0000000..d77b62b --- /dev/null +++ b/src/types/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/src/utils/RSA.js b/src/utils/RSA.js new file mode 100644 index 0000000..22de18b --- /dev/null +++ b/src/utils/RSA.js @@ -0,0 +1,30 @@ +import { JSEncrypt } from "jsencrypt"; +import const_num from "./const_num"; + +export default { + /** + * 闈炲绉板姞瀵嗙畻娉�-鍔犲瘑 + * @param word 闇�瑕佸姞瀵嗙殑瀛楃涓� + * @returns {string | false} + */ + encrypt(word) { + let encryptor = new JSEncrypt(); + let publicKey = const_num.publicKey; + encryptor.setPublicKey(publicKey); + return encryptor.encrypt(word); + }, + /** + * 闈炲绉板姞瀵嗙畻娉�-瑙e瘑 + * @param word + * @param privateKey + * @returns {string | false} + */ + decrypt(word, privateKey) { + if (!privateKey) { + return "璇峰啓鍏ョ閽�"; + } + let decrypt = new JSEncrypt(); + decrypt.setPrivateKey(privateKey); + return decrypt.decrypt(word); + }, +}; diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..981f7a1 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,16 @@ +// 璁块棶 token 缂撳瓨鐨� key +const ACCESS_TOKEN_KEY = "access_token"; + +function getAccessToken(): string { + return localStorage.getItem(ACCESS_TOKEN_KEY) || ""; +} + +function setAccessToken(token: string) { + localStorage.setItem(ACCESS_TOKEN_KEY, token); +} + +function clearToken() { + localStorage.removeItem(ACCESS_TOKEN_KEY); +} + +export { getAccessToken, setAccessToken, clearToken }; diff --git a/src/utils/const_num.js b/src/utils/const_num.js new file mode 100644 index 0000000..6190a27 --- /dev/null +++ b/src/utils/const_num.js @@ -0,0 +1,40 @@ +export default { + //privateKey: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI+kneP5IdDr2Wqr5KF"+ + // "Xt14DjRj7ytitrquvhF44n5QTt6Ty2wkmtzLXEw7VKimEl0ddfBI1tlaGclb0aUirVSboimWTAVz8C3Kh4kM/cQm1RmAjkfp"+ + // "9LPQE8sUefXwLQ3HPUGvXT1TTsOpB0yxqtwGYpdhDD7ahk7PQqkDlvQVrAgMBAAECgYA8ASdX4W2n6a4kKnRSleLqqg8aHazqAP"+ + // "vTinmAJqU65VW02SJ42yxyV3gFnTSErXfIfxviO3/U+0ruWiFVEwV5oDEh0dOd+HHGm4YzFXIRglMeRBgLuVJ+owzoVDwZsti"+ + // "IBa69DIjaJtmpSf5FjwxAth+gtCv3e11IXHraKN720QJBAMPMB1WtmpRGYHxWVYjKSL+RGw+h3gMQLk3exZjhmYRlXuqfVZ2Zol+NazDc59"+ + // "K5f+geMdJ0/X2kKnKLVjWzYHMCQQC7z1cFYswtLemxGfj+dwlVC01VL4pKa7HGHl/FAQ2UNYZY2d5hE/nXYbTpfI0gMowX926/aFpia7Nb"+ + // "AUJO7WEpAkAyUFa+LJthaOhYazMVsK2bFKW4kabkcJ8Fga6TR73UaNxIPGOa2SUBmuylpM6ptuNoeYHiDBAr3ijOQIIJ0KuDAkBy9fPahCNe9F+73"+ + // "J4hhVPdDtIDdto7u7hSAX215XMeabUW5iXNXqDsSg6nbWolb0t50CemWoYZALwE1Lx1+7AhAkEAoZtFt+2skjAxHEqNUye4vKBqB2Ng/wmfitCfT34"+ + // "lXWQsxs4BGk/8eQMzkam9bcB7FcinolxHF/1UjsUYpI+AgA==", + //privateKey: "", + //publicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCPpJ3j+SHQ69lqq+ShV7deA40Y+8rYra6rr4ReOJ+UE7ek8tsJJrcy1xMO1SophJdH"+ + // "XXwSNbZWhnJW9GlIq1Um6IplkwFc/AtyoeJDP3EJtUZgI5H6fSz0BPLFHn18C0Nxz1Br109U07DqQdMsarcBmKXYQw+2oZOz0KpA5b0FawIDAQAB" + publicKey: "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAu7CdCMIoWXUX584JpmE1bTE1r1MtmZGswsSbsm4s9zqETQ4BmUT2kz/wvvGsI8" + + "T3ZvTT4KIyKP4Ez+yNVejMM5XwR95KF4e3UwMc724buWKl4pVL09kvkCZt8ZKf359VAvhyHHz80wIiVmJs6xbho7OBsv/s7Hwho0n4HPL4u/eNR5vWa" + + "2rtgQG1+fi1XP0UiSRKKW15Va9R2CI3zB+sffquhyX5fi+06NibWzk7OPU+EGvAwkaJtrmfLAvpwr4+G0MBLIsPVFV17Sgpoj62rtfbhHwmLSo1JTw+" + + "/JskDQOjxXfw+w3uHAZgPTlEmn2Ya9ssIljqCBfvM9nbGUWcnmppKlPm6kECa4RsgPiRgPFV+nT/Q98kfUTb798Sy63x4NIZkLQn1DDbmcAgUqLR6y" + + "1r0fD8Ne3vVtuZlVR/8ZlcRAfb+th2cNN0rytrnUreJo7kPtTFdkNtmj0KdUkRO8ea0YymEQal+b0tCl3V8osSy+qO2OVRd7yCvpOWEYOBAgMBAAE=", + privateKey: "MIIG/gIBADANBgkqhkiG9w0BAQEFAASCBugwggbkAgEAAoIBgQC7sJ0IwihZdRfnzgmmYTVtMTWvUy2ZkazCxJuybiz3OoRNDgGZRPaT" + + "P/C+8awjxPdm9NPgojIo/gTP7I1V6MwzlfBH3koXh7dTAxzvbhu5YqXilUvT2S+QJm3xkp/fn1UC+HIcfPzTAiJWYmzrFuGjs4Gy/+zsfCGjSfgc8" + + "vi7941Hm9Zrau2BAbX5+LVc/RSJJEopbXlVr1HYIjfMH6x9+q6HJfl+L7To2JtbOTs49T4Qa8DCRom2uZ8sC+nCvj4bQwEsiw9UVXXtKCmiPrau19u" + + "EfCYtKjUlPD78myQNA6PFd/D7De4cBmA9OUSafZhr2ywiWOoIF+8z2dsZRZyeamkqU+bqQQJrhGyA+JGA8VX6dP9D3yR9RNvv3xLLrfHg0hmQtCfUM" + + "NuZwCBSotHrLWvR8Pw17e9W25mVVH/xmVxEB9v62HZw03SvK2udSt4mjuQ+1MV2Q22aPQp1SRE7x5rRjKYRBqX5vS0KXdXyixLL6o7Y5VF3vIK+k5Y" + + "Rg4ECAwEAAQKCAYEAsVBm6pFSwUCn9uxlnXOUn7WvvBTerYg8KDzJwsXnYSE9P/aNeBj4wZ/Udu+l6pz4BaIiUMOqk6N3NF+MHq8xy0JJn/vXD2e9v" + + "4TfXysssfUKNodI/bjfAFXt5BzbQM4r6ASC+Xry9v27JtURhP826Ap763lwgPG8baFB70dzyVBTfmUxKoX4HrpZCvD1lgXZ0r4f+gdca6CXt5KMGCGM" + + "OfAL1c4AaD/1r0yDaKkm0+aMXcMOdfthuuieAWS30K0cK7A2GIHmWWwvNrGW/EqIHUKLBIdrXo9bDq5X1vMnw9FoCfjEwgCtlsGHMbgoVt6Zd3ziNjc" + + "NQ/nVQZEBLmyz1pVL+RixxZN/RCEyQSM+0EBeJ4VV75unTvz+qT05r6Lv07L66pAt7VPfW5705e6SU1IPUezZhAqh+fY33szloFCEWVP+n2szEZYSYM" + + "8ZZW+ctdzjyj3SNJc6Z0eh6MPBQzTTz4Rk+NXpHnyqBONRsIFvbihewO+7j97Ct/RshhYBAoHBAO84WM29k7Aqochn6TUZC7rmwtmJNNRFIkkxVSj0N" + + "Te8cHvl+qWDooSHnTJSMm/xyvzYa3KOaKEviegPXuVMYLkZD1i4Wd58YzcC2fuAHcL6F41ZhrbD+GOpitQhmaud2RCm6gxWlylzuuUPtubnLipoy0WR" + + "b3X+C9ub+GMmAP+KLsEeWZUxAv806MsTqNJqagHLmvIcshnaKrxbReCorvQfIotQ/NdSw6/tJfHSPAG0KrccB9KSUxjLmHaOYLIHkQKBwQDI2vGAzHq" + + "TypoL47I7cOLDzVZ2KammhJg2wwyJdCWSy2OzJvnrgJaqOrQtVNUO8EJn5cLMlJDBFz7rbMvoO76oF/22CMNb8bhv3wh1yQfZ/IKGunrSSFH0jtTO2b" + + "fSQJM9sgJzMYLiOEy1yaPXZ43QeoVsoDQOKHGh3dF8Y6aTkA5FC9x4pmPzpRAgH5/X3THj6BIWbEI5BRiywZHWSboymF+DkT2NDDPoMQ9zML/04O30T" + + "uy8DbFf9xTw3wroJPECgcBAJdX6ZcnCxcvYV7T7nhm9JsA9YUOfYGKPSgFSGBplNczcDJGn7KKZ81u98LjBuA78unQlpfZ8sqjCZ8zEpDSTrhqladn/" + + "hU99ovAdNv/EFxhVuRoczHRBFWe69r+ke5GHm5rLcDTc0sHdRtd/F6MTkEJiB1viQhuf6jUzMS+3VrCu7JqNHTV2hhOe0UjGE+8VSCnmnrdLo2suUzN" + + "ryRAROoAi57bFbtY2yNsR+5RHyK5jp8qZNs+9qGrb79YSJ2ECgcAHZTBRKrY0rNgBKhAM6jofNXdCgIQzklw8X/AdO36KqhxwozW+ewyRFfo+VQpHM4d" + + "uZeJHQA0YXu+9IVNcqJ57d+6qfiYbQ4oj7FVWaOF2IDr6FPGivnDuDTg+qXuALUp+kghPD3qfM613YAY9Tx3EmE5DUp64CrssV4t4Bf9DHaG43xfuBUp" + + "W1TQDysZK32UP3CKWWsQRb2OaaVAiULKfXEbgBD/86n8axHuqJRhcPs/kF+fVgLeQLfvCZqPzKjECgcEAvBdzAp/vK/C8FYa3JVU1zcQKAIzv5HLyjL" + + "MCYYDkz5z7YcscJ3JUp+euIYem0i1hE3a3JpB5iqy92CauF1zZTb7Kqom8kes4fPf3PnDllIS2wMagPAMPubO7nuN9fa6KOoX//vwqvq+WnbLIXtPoO" + + "laxLdUud5gIRgVTz+0qc+8e5CUvv4+EQ2dUDkZLNFHPgvCFTD2ylEC3DXC0q4vWLZ7o14zniuhE3M3Kpf3UIIAFdzRlUv2n38kbXOUTxuZm" +} diff --git a/src/utils/downloadFile.ts b/src/utils/downloadFile.ts new file mode 100644 index 0000000..61cbd48 --- /dev/null +++ b/src/utils/downloadFile.ts @@ -0,0 +1,27 @@ +function downloadFile(url: string) { + return new Promise<void>((resolve, reject) => { + let baseUrl = ""; + if (process.env.NODE_ENV === "development") { + // 璺ㄥ煙璇锋眰 + baseUrl = "http://localhost:8080/bg/"; + } else { + baseUrl = location.protocol + "//" + location.host + "/bg/"; + } + const link = document.createElement("a"); + link.href = baseUrl + url; + // 1. 棰勬鏂囦欢鏄惁瀛樺湪 + fetch(link.href, { method: "HEAD" }) + .then(() => { + link.download = "鏂囦欢"; + document.body.appendChild(link); // 娣诲姞鍒� DOM + link.click(); + window.URL.revokeObjectURL(url); + resolve(); + }) + .catch(() => { + reject(); + }); + }); +} + +export default downloadFile; diff --git a/src/utils/formatPassword.js b/src/utils/formatPassword.js new file mode 100644 index 0000000..279a39a --- /dev/null +++ b/src/utils/formatPassword.js @@ -0,0 +1,9 @@ +import { md5 } from "js-md5"; +import RSA from "./RSA"; + +function formatPassword(pwd) { + let password = pwd + "&&&&&&&&&&" + md5(pwd); + return RSA.encrypt(password); +} + +export default formatPassword; diff --git a/src/utils/getLabelByValue.ts b/src/utils/getLabelByValue.ts new file mode 100644 index 0000000..f6ffc8e --- /dev/null +++ b/src/utils/getLabelByValue.ts @@ -0,0 +1,18 @@ +interface LabelValue { + value: number | string; + label: string; +} + +function getLabelByValue<T extends LabelValue>(value: number | string, list: T[], msg: T | null) { + let result = msg ? msg : "鏈煡"; + for (let i = 0; i < list.length; i++) { + let item = list[i]; + if (item.value === value) { + result = item.label; + break; + } + } + return result; +} + +export default getLabelByValue; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..44bb588 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,41 @@ +/** + * Check if an element has a class + * @param {HTMLElement} ele + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele: HTMLElement, cls: string) { + return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); +} + +/** + * Add class to element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function addClass(ele: HTMLElement, cls: string) { + if (!hasClass(ele, cls)) ele.className += " " + cls; +} + +/** + * Remove class from element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function removeClass(ele: HTMLElement, cls: string) { + if (hasClass(ele, cls)) { + const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); + ele.className = ele.className.replace(reg, " "); + } +} + +/** + * 鍒ゆ柇鏄惁鏄閮ㄩ摼鎺� + * + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path: string) { + const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); + return isExternal; +} diff --git a/src/utils/nprogress.ts b/src/utils/nprogress.ts new file mode 100644 index 0000000..c1d5f23 --- /dev/null +++ b/src/utils/nprogress.ts @@ -0,0 +1,18 @@ +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; + +// 杩涘害鏉� +NProgress.configure({ + // 鍔ㄧ敾鏂瑰紡 + easing: "ease", + // 閫掑杩涘害鏉$殑閫熷害 + speed: 500, + // 鏄惁鏄剧ず鍔犺浇ico + showSpinner: false, + // 鑷姩閫掑闂撮殧 + trickleSpeed: 200, + // 鍒濆鍖栨椂鐨勬渶灏忕櫨鍒嗘瘮 + minimum: 0.3, +}); + +export default NProgress; diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..30bfe42 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,38 @@ +import axios from "axios"; + +// 鍒涘缓 axios 瀹炰緥 +const service = axios.create({ + timeout: 50000, +}); + +if (import.meta.env.VITE_NODE_ENV === "development") { + // 璺ㄥ煙璇锋眰 + service.defaults.baseURL = "http://localhost:8080/bg/"; + service.defaults.withCredentials = true; // 淇濇寔璇锋眰澶� +} else { + service.defaults.baseURL = location.protocol + "//" + location.host + "/bg/"; +} +console.log(import.meta.env); +console.log(service.defaults); + +// 璇锋眰鎷︽埅鍣� +service.interceptors.request.use( + (config) => { + return config; + }, + (error) => Promise.reject(error) +); + +// 鍝嶅簲鎷︽埅鍣� +service.interceptors.response.use( + (response) => { + const { data } = response; + // 瀵瑰搷搴旀暟鎹仛鐐逛粈涔� + return data; + }, + (error) => { + return Promise.reject(error); + } +); + +export default service; diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..a62d01d --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,52 @@ +// 杈呭姪鍑芥暟锛氬皢鍗佸叚杩涘埗棰滆壊杞崲涓� RGB +function hexToRgb(hex: string): [number, number, number] { + const bigint = parseInt(hex.slice(1), 16); + return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; +} + +// 杈呭姪鍑芥暟锛氬皢 RGB 杞崲涓哄崄鍏繘鍒堕鑹� +function rgbToHex(r: number, g: number, b: number): string { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +} + +// 杈呭姪鍑芥暟锛氳皟鏁撮鑹蹭寒搴� +function adjustBrightness(hex: string, factor: number): string { + const rgb = hexToRgb(hex); + const newRgb = rgb.map((val) => + Math.max(0, Math.min(255, Math.round(val + (255 - val) * factor))) + ) as [number, number, number]; + return rgbToHex(...newRgb); +} + +export function generateThemeColors(primary: string) { + const colors: Record<string, string> = { + primary, + }; + + // 鐢熸垚娴呰壊鍙樹綋 + for (let i = 1; i <= 9; i++) { + const factor = i * 0.1; + colors[`primary-light-${i}`] = adjustBrightness(primary, factor); + } + + // 鐢熸垚娣辫壊鍙樹綋 + colors["primary-dark-2"] = adjustBrightness(primary, -0.2); + + return colors; +} + +export function applyTheme(colors: Record<string, string>) { + const el = document.documentElement; + + Object.entries(colors).forEach(([key, value]) => { + el.style.setProperty(`--el-color-${key}`, value); + }); +} + +export function toggleDarkMode(isDark: boolean) { + if (isDark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } +} diff --git a/src/views/dashboard/compoents/downloadFile.vue b/src/views/dashboard/compoents/downloadFile.vue new file mode 100644 index 0000000..03af12b --- /dev/null +++ b/src/views/dashboard/compoents/downloadFile.vue @@ -0,0 +1,133 @@ +<script setup lang="ts"> +import type { FormInstance } from "element-plus"; +import downloadFile from "@/utils/downloadFile"; +const props = defineProps(["data"]); +const emit = defineEmits(["update:close", "success"]); +const formRef = ref(); +const paramsRef = ref({ + snCode: "", + serialNumber: "", + materialCode: "", +}); +const rulesRef = ref({ + snCode: [ + { required: true, message: "璇疯緭鍏N鐮�", trigger: "blur" }, + { + validator: (rule: any, value: any, callback: Function) => { + if (value !== props.data.snCode) { + callback(new Error("SN鐮佷笌褰撳墠鍖匰N鐮佷笉鍖归厤锛侊紒锛�")); + } else { + callback(); + } + }, + trigger: "blur", + }, + ], + serialNumber: [ + { required: true, message: "璇疯緭鍏ュ簭鍒楀彿", trigger: "blur" }, + { + validator: (rule: any, value: any, callback: Function) => { + if (value !== props.data.serialNumber) { + callback(new Error("搴忓垪鍙蜂笌褰撳墠鍖呭簭鍒楀彿涓嶅尮閰嶏紒锛侊紒")); + } else { + callback(); + } + }, + trigger: "blur", + }, + ], + materialCode: [ + { required: true, message: "璇疯緭鍏ョ墿鏂欑紪鐮�", trigger: "blur" }, + { + validator: (rule: any, value: any, callback: Function) => { + if (value !== props.data.materialCode) { + callback(new Error("鐗╂枡缂栫爜涓庡綋鍓嶅寘鐗╂枡缂栫爜涓嶅尮閰嶏紒锛侊紒")); + } else { + callback(); + } + }, + trigger: "blur", + }, + ], +}); +const layout = reactive({ + gutter: 16, + span: 24, +}); + +// 鎻愪氦琛ㄥ崟 +function submitForm(formEl: FormInstance) { + if (!formEl) return; + formEl.validate((valid, fields) => { + if (valid) { + downloadFile(props.data.fileUrl) + .then(() => { + ElMessage({ + message: "涓嬭浇鎴愬姛锛侊紒锛�", + type: "success", + }); + close(); + }) + .catch(() => { + ElMessage({ + message: "涓嬭浇澶辫触, 璇锋鏌ョ綉缁滐紒锛侊紒", + type: "error", + }); + }); + } else { + ElMessage({ + message: "瀛樺湪涓嶅悎娉曟暟鎹紝璇蜂慨鏀癸紒锛侊紒", + type: "warning", + }); + console.log("error submit!", fields); + } + }); +} + +function close() { + emit("update:close", false); +} +</script> + +<template> + <div class="dialog-wrapper"> + <el-form + ref="formRef" + label-position="top" + :model="paramsRef" + :rules="rulesRef" + label-width="auto" + > + <el-row :gutter="layout.gutter"> + <el-col :span="layout.span"> + <el-form-item label="SN鐮�" prop="snCode"> + <el-input v-model="paramsRef.snCode"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="鐗╂枡缂栫爜" prop="materialCode"> + <el-input v-model="paramsRef.materialCode"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="搴忓垪鍙�" prop="serialNumber"> + <el-input v-model="paramsRef.serialNumber"></el-input> + </el-form-item> + </el-col> + </el-row> + <div class="from-footer"> + <el-button type="primary" @click="submitForm(formRef)">纭畾</el-button> + <el-button type="danger" @click="close">鍙栨秷</el-button> + </div> + </el-form> + </div> +</template> + +<style scoped lang="scss"> +.dialog-wrapper { + width: 580px; + .from-footer { + text-align: right; + } +} +</style> diff --git a/src/views/dashboard/compoents/uploadSoftware.vue b/src/views/dashboard/compoents/uploadSoftware.vue new file mode 100644 index 0000000..f73eeb4 --- /dev/null +++ b/src/views/dashboard/compoents/uploadSoftware.vue @@ -0,0 +1,161 @@ +<script setup lang="ts"> +import { UploadFilled } from "@element-plus/icons-vue"; +import { genFileId } from "element-plus"; +import type { FormInstance, UploadProps, UploadRawFile } from "element-plus"; +import SoftwareApi from "@/api/software"; +const emit = defineEmits(["update:close", "success"]); + +const paramsRef = ref({ + snCode: "", + serialNumber: "", + materialCode: "", +}); +const rulesRef = ref({ + snCode: [{ required: true, message: "璇疯緭鍏N鐮�", trigger: "blur" }], + serialNumber: [{ required: true, message: "璇疯緭鍏ュ簭鍒楀彿", trigger: "blur" }], + materialCode: [{ required: true, message: "璇疯緭鍏ョ墿鏂欑紪鐮�", trigger: "blur" }], +}); +const layout = reactive({ + gutter: 16, + span: 24, +}); + +const fileList = ref([]); +const uploadRef = ref(); +const formRef = ref(); +const handleExceed: UploadProps["onExceed"] = (files) => { + uploadRef.value!.clearFiles(); + const file = files[0] as UploadRawFile; + file.uid = genFileId(); + uploadRef.value!.handleStart(file); +}; + +// 鎻愪氦琛ㄥ崟 +function submitForm(formEl: FormInstance) { + if (fileList.value.length === 0) { + ElMessage({ + message: "璇烽�夋嫨涓婁紶鐨勬枃浠讹紒锛侊紒", + type: "error", + }); + return; + } + if (!formEl) return; + formEl.validate((valid, fields) => { + if (valid) { + ElMessageBox.confirm("纭涓婁紶杞欢鍗囩骇鍖�?", "绯荤粺鎻愮ず", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "info", + }) + .then(() => { + uploadFile(); + }) + .catch(() => {}); + } else { + ElMessage({ + message: "瀛樺湪涓嶅悎娉曟暟鎹紝璇蜂慨鏀癸紒锛侊紒", + type: "warning", + }); + console.log("error submit!", fields); + } + }); +} + +function uploadFile() { + const params = getParams(); + SoftwareApi.uploadSoftware(params) + .then((result) => { + const { code } = result; + if (code === 1) { + ElMessage({ + message: "涓婁紶鎴愬姛锛侊紒锛�", + type: "success", + }); + emit("success", true); + } else { + ElMessage({ + message: "涓婁紶澶辫触锛侊紒锛�", + type: "success", + }); + } + }) + .catch((error) => { + ElMessage({ + message: error, + type: "error", + }); + }) + .finally(() => {}); +} + +function getParams(): FormData { + const formData = new FormData(); + formData.append("file", fileList.value[0].raw); + formData.append("softwareStr", JSON.stringify(paramsRef.value)); + return formData; +} + +function close() { + emit("update:close", false); +} +</script> + +<template> + <div class="dialog-wrapper"> + <el-form + ref="formRef" + label-position="top" + :model="paramsRef" + :rules="rulesRef" + label-width="auto" + > + <el-row :gutter="layout.gutter"> + <el-col :span="layout.span"> + <el-form-item label="SN鐮�" prop="snCode"> + <el-input v-model="paramsRef.snCode"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="鐗╂枡缂栫爜" prop="materialCode"> + <el-input v-model="paramsRef.materialCode"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="搴忓垪鍙�" prop="serialNumber"> + <el-input v-model="paramsRef.serialNumber"></el-input> + </el-form-item> + </el-col> + <el-col :span="24"> + <el-upload + ref="uploadRef" + v-model:file-list="fileList" + drag + class="upload-demo" + action="#" + :auto-upload="false" + :on-exceed="handleExceed" + :limit="1" + > + <el-icon class="el-icon--upload"> + <upload-filled /> + </el-icon> + <div class="el-upload__text">鐐瑰嚮涓婁紶/绉婚櫎鏂囦欢</div> + </el-upload> + </el-col> + </el-row> + <div class="from-footer"> + <el-button type="primary" @click="submitForm(formRef)">纭畾</el-button> + <el-button type="danger" @click="close">鍙栨秷</el-button> + </div> + </el-form> + </div> +</template> + +<style scoped lang="scss"> +.dialog-wrapper { + width: 580px; + .from-footer { + text-align: right; + } +} +</style> diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..aedb039 --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,227 @@ +<template> + <div class="app-container"> + <!-- 鐢ㄦ埛鍒楄〃 --> + <el-col :lg="24" :xs="24"> + <div class="search-bar"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item label="SN鐮侊細" prop="status"> + <el-select + v-model="queryParams.snCode" + filterable + placeholder="鍏ㄩ儴" + clearable + class="!w-[200px]" + @change="handleQuery" + > + <el-option label="鍏ㄩ儴" value="" /> + <el-option + v-for="item in snCodeList" + :key="item" + :label="item" + :value="item" + ></el-option> + </el-select> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="search" @click="handleQuery">鎼滅储</el-button> + <el-button icon="refresh" @click="handleResetQuery">閲嶇疆</el-button> + <el-button + v-hasPerm="['sys:user:upload']" + type="success" + icon="upload" + @click="uploadDialog = true" + > + 涓婁紶 + </el-button> + </el-form-item> + </el-form> + </div> + </el-col> + <el-card shadow="never"> + <el-table v-loading="loading" :data="pageData"> + <el-table-column + show-overflow-tooltip + label="SN鐮�" + min-width="210" + align="center" + prop="snCode" + /> + <el-table-column + show-overflow-tooltip + label="鐗╂枡缂栫爜" + min-width="210" + align="center" + prop="materialCode" + /> + <el-table-column + show-overflow-tooltip + label="搴忓垪鍙�" + min-width="210" + align="center" + prop="serialNumber" + /> + <el-table-column + show-overflow-tooltip + label="杞欢鍖呬笂浼犳椂闂�" + width="210" + align="center" + prop="createTime" + /> + <el-table-column + show-overflow-tooltip + label="杞欢鍖呬笂浼犱汉" + width="210" + align="center" + prop="uploadUserName" + /> + <el-table-column align="center" label="鎿嶄綔" fixed="right" width="220"> + <template #default="{ row }"> + <el-button + type="primary" + icon="download" + link + size="small" + @click="handleDownload(row)" + > + 涓嬭浇 + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-if="total > 0" + v-model:total="total" + v-model:page="queryParams.pageNum" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </el-card> + + <el-dialog v-model="uploadDialog" draggable title="杞欢鍖呬笂浼�" align-center width="auto"> + <upload-software + v-if="uploadDialog" + v-model:close="uploadDialog" + @success="handleSuccess" + ></upload-software> + </el-dialog> + + <el-dialog v-model="downloadDialog" draggable title="杞欢鍖呬笅杞�" align-center width="auto"> + <download-file + v-if="downloadDialog" + v-model:close="downloadDialog" + :data="rowRef" + ></download-file> + </el-dialog> + </div> +</template> + +<script setup lang="ts"> +import SoftwareApi, { SoftwareInfo, SearchParams } from "@/api/software"; +import UploadSoftware from "@/views/dashboard/compoents/uploadSoftware.vue"; +import DownloadFile from "@/views/dashboard/compoents/downloadFile.vue"; + +defineOptions({ + name: "Dashboard", + inheritAttrs: false, +}); + +const queryParams = reactive<SearchParams>({ + snCode: "", + materialCode: "", + serialNumber: "", + pageNum: 1, + pageSize: 20, +}); +const pageData = ref<SoftwareInfo[]>(); +const total = ref(0); +const loading = ref(false); + +function handleQuery() { + loading.value = true; + SoftwareApi.search(queryParams) + .then((result) => { + const { code, data } = result; + if (code === 1) { + pageData.value = data.list; + total.value = data.total; + } else { + pageData.value = []; + total.value = 0; + } + }) + .finally(() => { + loading.value = false; + }); +} + +const queryFormRef = ref(); +function handleResetQuery() { + queryFormRef.value.resetFields(); + queryParams.pageNum = 1; + queryParams.snCode = ""; + queryParams.serialNumber = ""; + queryParams.materialCode = ""; + handleQuery(); +} + +const snCodeList = ref<string[]>([]); +function searchSnCode() { + SoftwareApi.searchSn().then((result) => { + const { code, data } = result; + if (code === 1) { + snCodeList.value = data; + } else { + snCodeList.value = []; + } + }); +} + +const materialList = ref<string[]>([]); +function searchMaterial() { + SoftwareApi.searchMaterial().then((result) => { + const { code, data } = result; + if (code === 1) { + materialList.value = data; + } else { + materialList.value = []; + } + }); +} + +const serialNumberList = ref<string[]>([]); +function searchSerialNumber() { + SoftwareApi.searchSerial().then((result) => { + const { code, data } = result; + if (code === 1) { + serialNumberList.value = data; + } else { + serialNumberList.value = []; + } + }); +} + +const uploadDialog = ref(false); + +function handleSuccess() { + uploadDialog.value = false; + handleQuery(); +} + +const downloadDialog = ref(false); +const rowRef = ref<SoftwareInfo>(); + +function handleDownload(data: SoftwareInfo) { + rowRef.value = data; + downloadDialog.value = true; +} + +onMounted(() => { + searchSnCode(); + searchMaterial(); + searchSerialNumber(); + handleQuery(); +}); +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/demo/api/apifox.vue b/src/views/demo/api/apifox.vue new file mode 100644 index 0000000..0eb334b --- /dev/null +++ b/src/views/demo/api/apifox.vue @@ -0,0 +1,27 @@ +<!-- 鎺ュ彛鏂囨。 --> +<template> + <div class="app-container"> + <iframe + src="https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5" + width="100%" + height="100%" + frameborder="0" + /> + </div> +</template> + +<style lang="scss" scoped> +/** 鍏抽棴tag鏍囩 */ +.app-container { + /* 50px = navbar = 50px */ + height: calc(100vh - 50px); +} + +/** 寮�鍚痶ag鏍囩 */ +.hasTagsView { + .app-container { + /* 84px = navbar + tags-view = 50px + 34px */ + height: calc(100vh - 84px); + } +} +</style> diff --git a/src/views/demo/internal-doc.vue b/src/views/demo/internal-doc.vue new file mode 100644 index 0000000..8ac1098 --- /dev/null +++ b/src/views/demo/internal-doc.vue @@ -0,0 +1,25 @@ +<template> + <div class="app-container"> + <iframe src="https://juejin.cn/post/7228990409909108793" frameborder="0" /> + </div> +</template> +<style lang="scss" scoped> +/** 鍏抽棴tag鏍囩 */ +.app-container { + /* 50px = navbar = 50px */ + height: calc(100vh - 50px); +} + +/** 寮�鍚痶ag鏍囩 */ +.hasTagsView { + .app-container { + /* 84px = navbar + tags-view = 50px + 34px */ + height: calc(100vh - 84px); + } +} + +iframe { + width: 100%; + height: 100%; +} +</style> diff --git a/src/views/demo/multi-level/children/children/level3-1.vue b/src/views/demo/multi-level/children/children/level3-1.vue new file mode 100644 index 0000000..888f58e --- /dev/null +++ b/src/views/demo/multi-level/children/children/level3-1.vue @@ -0,0 +1,5 @@ +<template> + <div style="padding: 30px"> + <el-alert :closable="false" title="鑿滃崟涓夌骇-1" type="error" /> + </div> +</template> diff --git a/src/views/demo/multi-level/children/children/level3-2.vue b/src/views/demo/multi-level/children/children/level3-2.vue new file mode 100644 index 0000000..a99c98e --- /dev/null +++ b/src/views/demo/multi-level/children/children/level3-2.vue @@ -0,0 +1,5 @@ +<template> + <div style="padding: 30px"> + <el-alert :closable="false" title="鑿滃崟涓夌骇-2" type="warning" /> + </div> +</template> diff --git a/src/views/demo/multi-level/children/level2.vue b/src/views/demo/multi-level/children/level2.vue new file mode 100644 index 0000000..abcc3a7 --- /dev/null +++ b/src/views/demo/multi-level/children/level2.vue @@ -0,0 +1,7 @@ +<template> + <div style="padding: 30px"> + <el-alert :closable="false" title="鑿滃崟浜岀骇" type="success"> + <router-view /> + </el-alert> + </div> +</template> diff --git a/src/views/demo/multi-level/level1.vue b/src/views/demo/multi-level/level1.vue new file mode 100644 index 0000000..6264f9f --- /dev/null +++ b/src/views/demo/multi-level/level1.vue @@ -0,0 +1,16 @@ +<template> + <div style="padding: 30px"> + <el-link + href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/multi-level/level1.vue" + type="primary" + target="_blank" + class="mb-10" + > + 绀轰緥婧愮爜 璇风偣鍑�>>>> + </el-link> + + <el-alert :closable="false" title="鑿滃崟涓�绾�"> + <router-view /> + </el-alert> + </div> +</template> diff --git a/src/views/error/401.vue b/src/views/error/401.vue new file mode 100644 index 0000000..c379ff0 --- /dev/null +++ b/src/views/error/401.vue @@ -0,0 +1,103 @@ +<script setup lang="ts"> +import { reactive, toRefs } from "vue"; +import { useRouter } from "vue-router"; + +defineOptions({ + name: "Page401", +}); + +const state = reactive({ + errGif: new URL("../../assets/images/401.gif", import.meta.url).href, + ewizardClap: "https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646", + dialogVisible: false, +}); + +const { errGif, ewizardClap, dialogVisible } = toRefs(state); + +const router = useRouter(); + +function back() { + router.back(); +} +</script> + +<template> + <div class="page-container"> + <el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">杩斿洖</el-button> + <el-row> + <el-col :span="12"> + <h1 class="text-jumbo text-ginormous">Oops!</h1> + gif鏉ユ簮 + <a href="https://zh.airbnb.com/" target="_blank">airbnb</a> + 椤甸潰 + <h2>浣犳病鏈夋潈闄愬幓璇ラ〉闈�</h2> + <h6>濡傛湁涓嶆弧璇疯仈绯讳綘棰嗗</h6> + <ul class="list-unstyled"> + <li>鎴栬�呬綘鍙互鍘�:</li> + <li class="link-type"> + <router-link to="/dashboard">鍥為椤�</router-link> + </li> + <li class="link-type"> + <a href="https://www.taobao.com/">闅忎究鐪嬬湅</a> + </li> + <li> + <a href="#" @click.prevent="dialogVisible = true">鐐规垜鐪嬪浘</a> + </li> + </ul> + </el-col> + <el-col :span="12"> + <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." /> + </el-col> + </el-row> + <el-dialog v-model="dialogVisible" title="闅忎究鐪�"> + <img :src="ewizardClap" class="pan-img" /> + </el-dialog> + </div> +</template> + +<style lang="scss" scoped> +.page-container { + width: 100%; + padding: 100px; + + .pan-back-btn { + color: #fff; + background: #008489; + border: none !important; + } + + .pan-gif { + display: block; + margin: 0 auto; + } + + .pan-img { + display: block; + width: 100%; + margin: 0 auto; + } + + .text-jumbo { + font-size: 60px; + font-weight: 700; + color: #484848; + } + + .list-unstyled { + font-size: 14px; + + li { + padding-bottom: 5px; + } + + a { + color: #008489; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } +} +</style> diff --git a/src/views/error/404.vue b/src/views/error/404.vue new file mode 100644 index 0000000..3334507 --- /dev/null +++ b/src/views/error/404.vue @@ -0,0 +1,243 @@ +<script setup lang="ts"> +import { useRouter } from "vue-router"; + +defineOptions({ + name: "Page404", +}); + +const router = useRouter(); + +function back() { + router.back(); +} +</script> + +<template> + <div class="page-container"> + <div class="pic-404"> + <img class="pic-404__parent" src="@/assets/images/404.png" alt="404" /> + <img class="pic-404__child left" src="@/assets/images/404_cloud.png" alt="404" /> + <img class="pic-404__child mid" src="@/assets/images/404_cloud.png" alt="404" /> + <img class="pic-404__child right" src="@/assets/images/404_cloud.png" alt="404" /> + </div> + <div class="bullshit"> + <div class="bullshit__oops">OOPS!</div> + <div class="bullshit__info"> + All rights reserved + <a style="color: #20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a> + </div> + <div class="bullshit__headline">The webmaster said that you can not enter this page...</div> + <div class="bullshit__info"> + Please check that the URL you entered is correct, or click the button below to return to the + homepage. + </div> + <a href="#" class="bullshit__return-home" @click.prevent="back">Back to home</a> + </div> + </div> +</template> + +<style lang="scss" scoped> +.page-container { + display: flex; + padding: 100px; + + .pic-404 { + width: 600px; + overflow: hidden; + + &__parent { + width: 100%; + } + + &__child { + &.left { + top: 17px; + left: 220px; + width: 80px; + opacity: 0; + animation-name: cloudLeft; + animation-duration: 2s; + animation-timing-function: linear; + animation-delay: 1s; + animation-fill-mode: forwards; + } + + &.mid { + top: 10px; + left: 420px; + width: 46px; + opacity: 0; + animation-name: cloudMid; + animation-duration: 2s; + animation-timing-function: linear; + animation-delay: 1.2s; + animation-fill-mode: forwards; + } + + &.right { + top: 100px; + left: 500px; + width: 62px; + opacity: 0; + animation-name: cloudRight; + animation-duration: 2s; + animation-timing-function: linear; + animation-delay: 1s; + animation-fill-mode: forwards; + } + + @keyframes cloudLeft { + 0% { + top: 17px; + left: 220px; + opacity: 0; + } + + 20% { + top: 33px; + left: 188px; + opacity: 1; + } + + 80% { + top: 81px; + left: 92px; + opacity: 1; + } + + 100% { + top: 97px; + left: 60px; + opacity: 0; + } + } + + @keyframes cloudMid { + 0% { + top: 10px; + left: 420px; + opacity: 0; + } + + 20% { + top: 40px; + left: 360px; + opacity: 1; + } + + 70% { + top: 130px; + left: 180px; + opacity: 1; + } + + 100% { + top: 160px; + left: 120px; + opacity: 0; + } + } + + @keyframes cloudRight { + 0% { + top: 100px; + left: 500px; + opacity: 0; + } + + 20% { + top: 120px; + left: 460px; + opacity: 1; + } + + 80% { + top: 180px; + left: 340px; + opacity: 1; + } + + 100% { + top: 200px; + left: 300px; + opacity: 0; + } + } + } + } + + .bullshit { + width: 300px; + padding: 30px 0; + overflow: hidden; + + &__oops { + margin-bottom: 20px; + font-size: 32px; + font-weight: bold; + line-height: 40px; + color: #1482f0; + opacity: 0; + animation-name: slideUp; + animation-duration: 0.5s; + animation-fill-mode: forwards; + } + + &__headline { + margin-bottom: 10px; + font-size: 20px; + font-weight: bold; + line-height: 24px; + color: #222; + opacity: 0; + animation-name: slideUp; + animation-duration: 0.5s; + animation-delay: 0.1s; + animation-fill-mode: forwards; + } + + &__info { + margin-bottom: 30px; + font-size: 13px; + line-height: 21px; + color: grey; + opacity: 0; + animation-name: slideUp; + animation-duration: 0.5s; + animation-delay: 0.2s; + animation-fill-mode: forwards; + } + + &__return-home { + display: block; + float: left; + width: 110px; + height: 36px; + font-size: 14px; + line-height: 36px; + color: #fff; + text-align: center; + cursor: pointer; + background: #1482f0; + border-radius: 100px; + opacity: 0; + animation-name: slideUp; + animation-duration: 0.5s; + animation-delay: 0.3s; + animation-fill-mode: forwards; + } + + @keyframes slideUp { + 0% { + opacity: 0; + transform: translateY(60px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } + } + } +} +</style> diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..838dc8b --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,311 @@ +<template> + <div class="login"> + <!-- 鐧诲綍椤靛ご閮� --> + <div class="login-header"> + <div class="flex-y-center"> + <el-switch + v-model="isDark" + inline-prompt + active-icon="Moon" + inactive-icon="Sunny" + @change="toggleTheme" + /> + </div> + </div> + + <!-- 鐧诲綍椤靛唴瀹� --> + <div class="login-form"> + <el-form ref="loginFormRef" :model="loginFormData" :rules="loginRules"> + <div class="form-title"> + <h2>杞欢绠$悊骞冲彴</h2> + </div> + + <!-- 鐢ㄦ埛鍚� --> + <el-form-item prop="username"> + <div class="input-wrapper"> + <el-icon class="mx-2"> + <User /> + </el-icon> + <el-input + ref="username" + v-model="loginFormData.username" + placeholder="鐢ㄦ埛鍚�" + name="username" + size="large" + class="h-[48px]" + /> + </div> + </el-form-item> + + <!-- 瀵嗙爜 --> + <el-tooltip :visible="isCapslock" content="澶у啓閿佸畾宸叉墦寮�" placement="right"> + <el-form-item prop="password"> + <div class="input-wrapper"> + <el-icon class="mx-2"> + <Lock /> + </el-icon> + <el-input + v-model="loginFormData.password" + placeholder="瀵嗙爜" + type="password" + name="password" + size="large" + class="h-[48px] pr-2" + show-password + @keyup="checkCapslock" + @keyup.enter="handleLoginSubmit" + /> + </div> + </el-form-item> + </el-tooltip> + + <!-- 鐧诲綍鎸夐挳 --> + <el-button + :loading="loading" + type="primary" + size="large" + class="w-full" + @click.prevent="handleLoginSubmit" + > + 鐧诲綍 + </el-button> + </el-form> + </div> + + <!-- 鐧诲綍椤靛簳閮� --> + <div class="login-footer"> + <el-text size="small"> + Copyright 漏 2021 - 2025 youlai.tech All Rights Reserved. + <a href="http://beian.miit.gov.cn/" target="_blank">鐨朓CP澶�20006496鍙�-2</a> + </el-text> + </div> + </div> +</template> + +<script setup lang="ts"> +import { LocationQuery, useRoute } from "vue-router"; +import router from "@/router"; + +import type { FormInstance } from "element-plus"; +import { ThemeMode } from "@/enums/settings/theme.enum"; + +import { useSettingsStore, useUserStore } from "@/store"; +import { LoginFormData } from "@/api/user"; + +const userStore = useUserStore(); +const settingsStore = useSettingsStore(); + +const route = useRoute(); +const loginFormRef = ref<FormInstance>(); + +const isDark = ref(settingsStore.theme === ThemeMode.DARK); // 鏄惁鏆楅粦妯″紡 +const loading = ref(false); // 鎸夐挳 loading 鐘舵�� +const isCapslock = ref(false); // 鏄惁澶у啓閿佸畾 + +const loginFormData = ref<LoginFormData>({ + username: "", + password: "", + captchaKey: "", + captchaCode: "", +}); + +const loginRules = computed(() => { + return { + username: [ + { + required: true, + trigger: "blur", + message: "璇疯緭鍏ョ敤鎴峰悕", + }, + ], + password: [ + { + required: true, + trigger: "blur", + message: "璇疯緭鍏ュ瘑鐮�", + }, + ], + }; +}); + +// 鐧诲綍 +async function handleLoginSubmit() { + loginFormRef.value?.validate((valid: boolean) => { + if (valid) { + loading.value = true; + userStore + .login(loginFormData.value) + .then(async () => { + // 璺宠浆鍒扮櫥褰曞墠鐨勯〉闈� + const { path, queryParams } = parseRedirect(); + console.log("璺宠浆鍒扮櫥褰曞墠鐨勯〉闈�", path, queryParams); + await router.push("/"); + }) + .catch(() => { + ElMessage({ + message: "鐧诲綍澶辫触锛岃鑱旂郴绠$悊鍛橈紒锛侊紒", + type: "error", + }); + }) + .finally(() => { + loading.value = false; + }); + } + }); +} + +/** + * 瑙f瀽 redirect 瀛楃涓� 涓� path 鍜� queryParams + * + * @returns { path: string, queryParams: Record<string, string> } 瑙f瀽鍚庣殑 path 鍜� queryParams + */ +function parseRedirect(): { + path: string; + queryParams: Record<string, string>; +} { + const query: LocationQuery = route.query; + const redirect = (query.redirect as string) ?? "/"; + + const url = new URL(redirect, window.location.origin); + const path = url.pathname; + const queryParams: Record<string, string> = {}; + + url.searchParams.forEach((value, key) => { + queryParams[key] = value; + }); + + return { path, queryParams }; +} + +// 涓婚鍒囨崲 +const toggleTheme = () => { + const newTheme = settingsStore.theme === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK; + settingsStore.changeTheme(newTheme); +}; + +// 妫�鏌ヨ緭鍏ュぇ灏忓啓 +function checkCapslock(event: KeyboardEvent) { + // 闃叉娴忚鍣ㄥ瘑鐮佽嚜鍔ㄥ~鍏呮椂鎶ラ敊 + if (event instanceof KeyboardEvent) { + isCapslock.value = event.getModifierState("CapsLock"); + } +} + +onMounted(() => {}); +</script> + +<style lang="scss" scoped> +.login { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 20px; + overflow-y: auto; + background: url("@/assets/images/login-bg.jpg") no-repeat center right; + + .login-header { + position: absolute; + top: 0; + display: flex; + justify-content: right; + width: 100%; + padding: 15px; + + .logo { + width: 26px; + height: 26px; + } + + .title { + margin: auto 5px; + font-size: 24px; + font-weight: bold; + color: #3b82f6; + } + } + + .login-form { + display: flex; + flex-direction: column; + justify-content: center; + width: 460px; + padding: 40px; + overflow: hidden; + background-color: #fff; + border-radius: 5px; + box-shadow: var(--el-box-shadow-light); + + @media (width <= 460px) { + width: 100%; + padding: 20px; + } + + .form-title { + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0 20px; + text-align: center; + } + + .input-wrapper { + display: flex; + align-items: center; + width: 100%; + } + + .captcha-img { + height: 48px; + cursor: pointer; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + .login-footer { + position: fixed; + bottom: 0; + width: 100%; + padding: 10px 0; + text-align: center; + } +} + +:deep(.el-form-item) { + background: var(--el-input-bg-color); + border: 1px solid var(--el-border-color); + border-radius: 5px; +} + +:deep(.el-input) { + .el-input__wrapper { + padding: 0; + background-color: transparent; + box-shadow: none; + + &.is-focus, + &:hover { + box-shadow: none !important; + } + + input:-webkit-autofill { + /* 閫氳繃寤舵椂娓叉煋鑳屾櫙鑹插彉鐩稿幓闄よ儗鏅鑹� */ + transition: background-color 1000s ease-in-out 0s; + } + } +} + +html.dark { + .login { + background: url("@/assets/images/login-bg-dark.jpg") no-repeat center right; + + .login-form { + background: transparent; + box-shadow: var(--el-box-shadow); + } + } +} +</style> diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue new file mode 100644 index 0000000..ad739c4 --- /dev/null +++ b/src/views/profile/index.vue @@ -0,0 +1,217 @@ +<template> + <div class="profile-container"> + <el-row :gutter="20"> + <!-- 鍙充晶淇℃伅鍗$墖 --> + <el-col :span="24"> + <el-card class="info-card"> + <template #header> + <div class="card-header"> + <span>璐﹀彿淇℃伅</span> + </div> + </template> + <el-descriptions :column="1" border> + <el-descriptions-item label="鐢ㄦ埛鍚�"> + {{ userProfile.name }} + <el-icon v-if="userProfile.sex === 1" class="gender-icon male"> + <Male /> + </el-icon> + <el-icon v-else class="gender-icon female"> + <Female /> + </el-icon> + </el-descriptions-item> + <el-descriptions-item label="鎵嬫満鍙风爜"> + {{ userProfile.phoneNumber || "鏈粦瀹�" }} + </el-descriptions-item> + <el-descriptions-item label="閭"> + {{ userProfile.email || "鏈粦瀹�" }} + </el-descriptions-item> + <el-descriptions-item label="鍒涘缓鏃堕棿"> + {{ userProfile.createTime }} + </el-descriptions-item> + </el-descriptions> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script lang="ts" setup> +import { UserInfo } from "@/api/user"; +import { useUserStore } from "@/store"; + +const { userInfo } = useUserStore(); +const userProfile = ref<UserInfo>({ + roles: [], + perms: [], +}); + +onMounted(async () => { + userProfile.value = userInfo; +}); +</script> + +<style lang="scss" scoped> +.profile-container { + min-height: calc(100vh - 84px); + padding: 20px; + background: var(--el-fill-color-blank); +} + +.user-card { + .user-info { + padding: 20px 0; + text-align: center; + + .avatar-wrapper { + position: relative; + display: inline-block; + margin-bottom: 16px; + + .avatar-edit-btn { + position: absolute; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + border: none; + transition: all 0.3s ease; + + &:hover { + background: rgba(0, 0, 0, 0.7); + } + } + } + + .user-name { + margin-bottom: 8px; + + .nickname { + font-size: 18px; + font-weight: 600; + color: var(--el-text-color-primary); + } + + .edit-icon { + margin-left: 8px; + color: var(--el-text-color-secondary); + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + color: var(--el-color-primary); + } + } + } + + .user-role { + font-size: 14px; + color: var(--el-text-color-secondary); + } + } + + .user-stats { + display: flex; + justify-content: space-around; + padding: 16px 0; + + .stat-item { + text-align: center; + + .stat-value { + font-size: 20px; + font-weight: 600; + color: var(--el-text-color-primary); + } + + .stat-label { + margin-top: 4px; + font-size: 12px; + color: var(--el-text-color-secondary); + } + } + } +} + +.info-card, +.security-card { + margin-bottom: 20px; + + .card-header { + font-size: 16px; + font-weight: 600; + color: var(--el-text-color-primary); + } +} + +.security-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0; + + .security-info { + .security-title { + margin-bottom: 4px; + font-size: 16px; + font-weight: 500; + color: var(--el-text-color-primary); + } + + .security-desc { + font-size: 14px; + color: var(--el-text-color-secondary); + } + } +} + +.el-descriptions { + .el-descriptions__label { + font-weight: 500; + color: var(--el-text-color-regular); + } + + .el-descriptions__content { + color: var(--el-text-color-primary); + } + + .gender-icon { + margin-left: 8px; + font-size: 16px; + + &.male { + color: #409eff; + } + + &.female { + color: #f56c6c; + } + } +} + +.el-dialog { + .el-dialog__header { + padding: 20px; + margin: 0; + border-bottom: 1px solid var(--el-border-color-light); + } + + .el-dialog__body { + padding: 30px 20px; + } + + .el-dialog__footer { + padding: 20px; + border-top: 1px solid var(--el-border-color-light); + } +} + +// 鍝嶅簲寮忛�傞厤 +@media (max-width: 768px) { + .profile-container { + padding: 10px; + } + + .el-col { + width: 100%; + } +} +</style> diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue new file mode 100644 index 0000000..2b61386 --- /dev/null +++ b/src/views/redirect/index.vue @@ -0,0 +1,15 @@ +<template> + <div /> +</template> + +<script setup lang="ts"> +import { useRoute, useRouter } from "vue-router"; + +const route = useRoute(); +const router = useRouter(); + +const { params, query } = route; +const { path } = params; + +router.replace({ path: "/" + path, query }); +</script> diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue new file mode 100644 index 0000000..0dba085 --- /dev/null +++ b/src/views/system/dept/index.vue @@ -0,0 +1,315 @@ +<template> + <div class="app-container"> + <div class="search-bar"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item label="鍏抽敭瀛�" prop="keywords"> + <el-input + v-model="queryParams.keywords" + placeholder="閮ㄩ棬鍚嶇О" + @keyup.enter="handleQuery" + /> + </el-form-item> + + <el-form-item label="閮ㄩ棬鐘舵��" prop="status"> + <el-select v-model="queryParams.status" placeholder="鍏ㄩ儴" clearable class="!w-[100px]"> + <el-option :value="1" label="姝e父" /> + <el-option :value="0" label="绂佺敤" /> + </el-select> + </el-form-item> + <el-form-item> + <el-button class="filter-item" type="primary" icon="search" @click="handleQuery"> + 鎼滅储 + </el-button> + <el-button icon="refresh" @click="handleResetQuery">閲嶇疆</el-button> + </el-form-item> + </el-form> + </div> + + <el-card shadow="never"> + <div class="mb-10px"> + <el-button + v-hasPerm="['sys:dept:add']" + type="success" + icon="plus" + @click="handleOpenDialog()" + > + 鏂板 + </el-button> + <el-button + v-hasPerm="['sys:dept:delete']" + type="danger" + :disabled="selectIds.length === 0" + icon="delete" + @click="handleDelete()" + > + 鍒犻櫎 + </el-button> + </div> + + <el-table + v-loading="loading" + :data="deptList" + row-key="id" + default-expand-all + :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" align="center" /> + <el-table-column prop="name" label="閮ㄩ棬鍚嶇О" min-width="200" /> + <el-table-column prop="code" label="閮ㄩ棬缂栧彿" width="200" /> + <el-table-column prop="status" label="鐘舵��" width="100"> + <template #default="scope"> + <el-tag v-if="scope.row.status == 1" type="success">姝e父</el-tag> + <el-tag v-else type="info">绂佺敤</el-tag> + </template> + </el-table-column> + + <el-table-column prop="sort" label="鎺掑簭" width="100" /> + + <el-table-column label="鎿嶄綔" fixed="right" align="left" width="200"> + <template #default="scope"> + <el-button + v-hasPerm="['sys:dept:add']" + type="primary" + link + size="small" + icon="plus" + @click.stop="handleOpenDialog(scope.row.id, undefined)" + > + 鏂板 + </el-button> + <el-button + v-hasPerm="['sys:dept:edit']" + type="primary" + link + size="small" + icon="edit" + @click.stop="handleOpenDialog(scope.row.parentId, scope.row.id)" + > + 缂栬緫 + </el-button> + <el-button + v-hasPerm="['sys:dept:delete']" + type="danger" + link + size="small" + icon="delete" + @click.stop="handleDelete(scope.row.id)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + </el-card> + + <el-dialog + v-model="dialog.visible" + :title="dialog.title" + width="600px" + @closed="handleCloseDialog" + > + <el-form ref="deptFormRef" :model="formData" :rules="rules" label-width="80px"> + <el-form-item label="涓婄骇閮ㄩ棬" prop="parentId"> + <el-tree-select + v-model="formData.parentId" + placeholder="閫夋嫨涓婄骇閮ㄩ棬" + :data="deptOptions" + filterable + check-strictly + :render-after-expand="false" + /> + </el-form-item> + <el-form-item label="閮ㄩ棬鍚嶇О" prop="name"> + <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" /> + </el-form-item> + <el-form-item label="閮ㄩ棬缂栧彿" prop="code"> + <el-input v-model="formData.code" placeholder="璇疯緭鍏ラ儴闂ㄧ紪鍙�" /> + </el-form-item> + <el-form-item label="鏄剧ず鎺掑簭" prop="sort"> + <el-input-number + v-model="formData.sort" + controls-position="right" + style="width: 100px" + :min="0" + /> + </el-form-item> + <el-form-item label="閮ㄩ棬鐘舵��"> + <el-radio-group v-model="formData.status"> + <el-radio :value="1">姝e父</el-radio> + <el-radio :value="0">绂佺敤</el-radio> + </el-radio-group> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-dialog> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "Dept", + inheritAttrs: false, +}); + +import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/system/dept.api"; + +const queryFormRef = ref(); +const deptFormRef = ref(); + +const loading = ref(false); +const selectIds = ref<number[]>([]); +const queryParams = reactive<DeptQuery>({}); + +const dialog = reactive({ + title: "", + visible: false, +}); + +const deptList = ref<DeptVO[]>(); +const deptOptions = ref<OptionType[]>(); +const formData = reactive<DeptForm>({ + status: 1, + parentId: "0", + sort: 1, +}); + +const rules = reactive({ + parentId: [{ required: true, message: "涓婄骇閮ㄩ棬涓嶈兘涓虹┖", trigger: "change" }], + name: [{ required: true, message: "閮ㄩ棬鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }], + code: [{ required: true, message: "閮ㄩ棬缂栧彿涓嶈兘涓虹┖", trigger: "blur" }], + sort: [{ required: true, message: "鏄剧ず鎺掑簭涓嶈兘涓虹┖", trigger: "blur" }], +}); + +// 鏌ヨ閮ㄩ棬 +function handleQuery() { + loading.value = true; + DeptAPI.getList(queryParams).then((data) => { + deptList.value = data; + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + handleQuery(); +} + +// 澶勭悊閫変腑椤瑰彉鍖� +function handleSelectionChange(selection: any) { + selectIds.value = selection.map((item: any) => item.id); +} + +/** + * 鎵撳紑閮ㄩ棬寮圭獥 + * + * @param parentId 鐖堕儴闂↖D + * @param deptId 閮ㄩ棬ID + */ +async function handleOpenDialog(parentId?: string, deptId?: string) { + // 鍔犺浇閮ㄩ棬涓嬫媺鏁版嵁 + const data = await DeptAPI.getOptions(); + deptOptions.value = [ + { + value: "0", + label: "椤剁骇閮ㄩ棬", + children: data, + }, + ]; + + dialog.visible = true; + if (deptId) { + dialog.title = "淇敼閮ㄩ棬"; + DeptAPI.getFormData(deptId).then((data) => { + Object.assign(formData, data); + }); + } else { + dialog.title = "鏂板閮ㄩ棬"; + formData.parentId = parentId || "0"; + } +} + +// 鎻愪氦閮ㄩ棬琛ㄥ崟 +function handleSubmit() { + deptFormRef.value.validate((valid: any) => { + if (valid) { + loading.value = true; + const deptId = formData.id; + if (deptId) { + DeptAPI.update(deptId, formData) + .then(() => { + ElMessage.success("淇敼鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } else { + DeptAPI.create(formData) + .then(() => { + ElMessage.success("鏂板鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } + } + }); +} + +// 鍒犻櫎閮ㄩ棬 +function handleDelete(deptId?: string) { + const deptIds = [deptId || selectIds.value].join(","); + + if (!deptIds) { + ElMessage.warning("璇峰嬀閫夊垹闄ら」"); + return; + } + + ElMessageBox.confirm("纭鍒犻櫎宸查�変腑鐨勬暟鎹」?", "璀﹀憡", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + }).then( + () => { + loading.value = true; + DeptAPI.deleteByIds(deptIds) + .then(() => { + ElMessage.success("鍒犻櫎鎴愬姛"); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + }, + () => { + ElMessage.info("宸插彇娑堝垹闄�"); + } + ); +} + +// 閲嶇疆琛ㄥ崟 +function resetForm() { + deptFormRef.value.resetFields(); + deptFormRef.value.clearValidate(); + + formData.id = undefined; + formData.parentId = "0"; + formData.status = 1; + formData.sort = 1; +} + +// 鍏抽棴寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + resetForm(); +} + +onMounted(() => { + handleQuery(); +}); +</script> diff --git a/src/views/system/dict/dict-item.vue b/src/views/system/dict/dict-item.vue new file mode 100644 index 0000000..9da8433 --- /dev/null +++ b/src/views/system/dict/dict-item.vue @@ -0,0 +1,285 @@ +<!-- 瀛楀吀椤� --> +<template> + <div class="app-container"> + <div class="search-bar mt-5"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item label="鍏抽敭瀛�" prop="keywords"> + <el-input + v-model="queryParams.keywords" + placeholder="瀛楀吀鏍囩/瀛楀吀鍊�" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="search" @click="handleQuery()">鎼滅储</el-button> + <el-button icon="refresh" @click="handleResetQuery()">閲嶇疆</el-button> + </el-form-item> + </el-form> + </div> + + <el-card shadow="never"> + <div class="mb-[10px]"> + <el-button type="success" icon="plus" @click="handleOpenDialog()">鏂板</el-button> + <el-button type="danger" :disabled="ids.length === 0" icon="delete" @click="handleDelete()"> + 鍒犻櫎 + </el-button> + </div> + + <el-table + v-loading="loading" + highlight-current-row + :data="tableData" + border + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" align="center" /> + <el-table-column label="瀛楀吀椤规爣绛�" prop="label" /> + <el-table-column label="瀛楀吀椤瑰��" prop="value" /> + <el-table-column label="鎺掑簭" prop="sort" /> + <el-table-column label="鐘舵��"> + <template #default="scope"> + <el-tag :type="scope.row.status === 1 ? 'success' : 'info'"> + {{ scope.row.status === 1 ? "鍚敤" : "绂佺敤" }} + </el-tag> + </template> + </el-table-column> + + <el-table-column fixed="right" label="鎿嶄綔" align="center" width="220"> + <template #default="scope"> + <el-button + type="primary" + link + size="small" + icon="edit" + @click.stop="handleOpenDialog(scope.row)" + > + 缂栬緫 + </el-button> + <el-button + type="danger" + link + size="small" + icon="delete" + @click.stop="handleDelete(scope.row.id)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-if="total > 0" + v-model:total="total" + v-model:page="queryParams.pageNum" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </el-card> + + <!--瀛楀吀椤瑰脊绐�--> + <el-dialog + v-model="dialog.visible" + :title="dialog.title" + width="820px" + @close="handleCloseDialog" + > + <el-form ref="dataFormRef" :model="formData" :rules="computedRules" label-width="100px"> + <el-card shadow="never"> + <el-form-item label="瀛楀吀椤规爣绛�" prop="label"> + <el-input v-model="formData.label" placeholder="璇疯緭鍏ュ瓧鍏告爣绛�" /> + </el-form-item> + <el-form-item label="瀛楀吀椤瑰��" prop="value"> + <el-input v-model="formData.value" placeholder="璇疯緭鍏ュ瓧鍏稿��" /> + </el-form-item> + <el-form-item label="鐘舵��"> + <el-radio-group v-model="formData.status"> + <el-radio :value="1">鍚敤</el-radio> + <el-radio :value="0">绂佺敤</el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="鎺掑簭"> + <el-input-number v-model="formData.sort" controls-position="right" /> + </el-form-item> + <el-form-item label="鏍囩绫诲瀷"> + <el-tag v-if="formData.tagType" :type="formData.tagType" class="mr-2"> + {{ formData.label }} + </el-tag> + <el-radio-group v-model="formData.tagType"> + <el-radio value="success" border size="small">success</el-radio> + <el-radio value="warning" border size="small">warning</el-radio> + <el-radio value="info" border size="small">info</el-radio> + <el-radio value="primary" border size="small">primary</el-radio> + <el-radio value="danger" border size="small">danger</el-radio> + <el-radio value="" border size="small">娓呯┖</el-radio> + </el-radio-group> + </el-form-item> + </el-card> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmitClick">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-dialog> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "DictItem", + inherititems: false, +}); + +import DictAPI, { DictItemPageQuery, DictItemPageVO, DictItemForm } from "@/api/system/dict.api"; + +const route = useRoute(); + +const dictCode = ref(route.query.dictCode as string); + +const queryFormRef = ref(); +const dataFormRef = ref(); + +const loading = ref(false); +const ids = ref<number[]>([]); +const total = ref(0); + +const queryParams = reactive<DictItemPageQuery>({ + pageNum: 1, + pageSize: 10, +}); + +const tableData = ref<DictItemPageVO[]>(); + +const dialog = reactive({ + title: "", + visible: false, +}); + +const formData = reactive<DictItemForm>({}); + +const computedRules = computed(() => { + const rules: Partial<Record<string, any>> = { + value: [{ required: true, message: "璇疯緭鍏ュ瓧鍏稿��", trigger: "blur" }], + label: [{ required: true, message: "璇疯緭鍏ュ瓧鍏告爣绛�", trigger: "blur" }], + }; + return rules; +}); + +// 鏌ヨ +function handleQuery() { + loading.value = true; + DictAPI.getDictItemPage(dictCode.value, queryParams) + .then((data) => { + tableData.value = data.list; + total.value = data.total; + }) + .finally(() => { + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + queryParams.pageNum = 1; + handleQuery(); +} + +// 琛岄�夋嫨 +function handleSelectionChange(selection: any) { + ids.value = selection.map((item: any) => item.id); +} + +// 鎵撳紑寮圭獥 +function handleOpenDialog(row?: DictItemPageVO) { + dialog.visible = true; + dialog.title = row ? "缂栬緫瀛楀吀椤�" : "鏂板瀛楀吀椤�"; + + if (row?.id) { + DictAPI.getDictItemFormData(dictCode.value, row.id).then((data) => { + Object.assign(formData, data); + }); + } +} + +// 鎻愪氦琛ㄥ崟 +function handleSubmitClick() { + dataFormRef.value.validate((isValid: boolean) => { + if (isValid) { + loading.value = true; + const id = formData.id; + formData.dictCode = dictCode.value; + if (id) { + DictAPI.updateDictItem(dictCode.value, id, formData) + .then(() => { + ElMessage.success("淇敼鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } else { + DictAPI.createDictItem(dictCode.value, formData) + .then(() => { + ElMessage.success("鏂板鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } + } + }); +} + +// 鍏抽棴寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + + dataFormRef.value.resetFields(); + dataFormRef.value.clearValidate(); + + formData.id = undefined; + formData.sort = 1; + formData.status = 1; +} +/** + * 鍒犻櫎瀛楀吀 + * + * @param id 瀛楀吀ID + */ +function handleDelete(id?: string) { + const itemIds = [id || ids.value].join(","); + if (!itemIds) { + ElMessage.warning("璇峰嬀閫夊垹闄ら」"); + return; + } + ElMessageBox.confirm("纭鍒犻櫎宸查�変腑鐨勬暟鎹」?", "璀﹀憡", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + }).then( + () => { + DictAPI.deleteDictItems(dictCode.value, itemIds).then(() => { + ElMessage.success("鍒犻櫎鎴愬姛"); + handleResetQuery(); + }); + }, + () => { + ElMessage.info("宸插彇娑堝垹闄�"); + } + ); +} + +onMounted(() => { + handleQuery(); +}); + +// 鍚屼竴璺敱鍙傛暟鍙樺寲鏃舵洿鏂版暟鎹� +onBeforeRouteUpdate((to) => { + queryParams.dictCode = to.query.dictCode as string; + handleQuery(); +}); +</script> diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue new file mode 100644 index 0000000..3945991 --- /dev/null +++ b/src/views/system/dict/index.vue @@ -0,0 +1,284 @@ +<!-- 瀛楀吀 --> +<template> + <div class="app-container"> + <div class="search-bar"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item label="鍏抽敭瀛�" prop="keywords"> + <el-input + v-model="queryParams.keywords" + placeholder="瀛楀吀鍚嶇О/缂栫爜" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="search" @click="handleQuery()">鎼滅储</el-button> + <el-button icon="refresh" @click="handleResetQuery()">閲嶇疆</el-button> + </el-form-item> + </el-form> + </div> + + <el-card shadow="never"> + <div class="mb-[10px]"> + <el-button type="success" icon="plus" @click="handleAddClick()">鏂板</el-button> + <el-button type="danger" :disabled="ids.length === 0" icon="delete" @click="handleDelete()"> + 鍒犻櫎 + </el-button> + </div> + + <el-table + v-loading="loading" + highlight-current-row + :data="tableData" + border + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" align="center" /> + <el-table-column label="瀛楀吀鍚嶇О" prop="name" /> + <el-table-column label="瀛楀吀缂栫爜" prop="dictCode" /> + <el-table-column label="鐘舵��" prop="status"> + <template #default="scope"> + <el-tag :type="scope.row.status === 1 ? 'success' : 'info'"> + {{ scope.row.status === 1 ? "鍚敤" : "绂佺敤" }} + </el-tag> + </template> + </el-table-column> + <el-table-column fixed="right" label="鎿嶄綔" align="center" width="220"> + <template #default="scope"> + <el-button type="primary" link size="small" @click.stop="handleOpenDictData(scope.row)"> + <template #icon> + <Collection /> + </template> + 瀛楀吀鏁版嵁 + </el-button> + + <el-button + type="primary" + link + size="small" + icon="edit" + @click.stop="handleEditClick(scope.row.id)" + > + 缂栬緫 + </el-button> + <el-button + type="danger" + link + size="small" + icon="delete" + @click.stop="handleDelete(scope.row.id)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-if="total > 0" + v-model:total="total" + v-model:page="queryParams.pageNum" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </el-card> + + <!--瀛楀吀寮圭獥--> + <el-dialog + v-model="dialog.visible" + :title="dialog.title" + width="500px" + @close="handleCloseDialog" + > + <el-form ref="dataFormRef" :model="formData" :rules="computedRules" label-width="100px"> + <el-card shadow="never"> + <el-form-item label="瀛楀吀鍚嶇О" prop="name"> + <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ瓧鍏稿悕绉�" /> + </el-form-item> + + <el-form-item label="瀛楀吀缂栫爜" prop="dictCode"> + <el-input v-model="formData.dictCode" placeholder="璇疯緭鍏ュ瓧鍏哥紪鐮�" /> + </el-form-item> + + <el-form-item label="鐘舵��"> + <el-radio-group v-model="formData.status"> + <el-radio :value="1">鍚敤</el-radio> + <el-radio :value="0">绂佺敤</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item label="澶囨敞"> + <el-input v-model="formData.remark" type="textarea" placeholder="璇疯緭鍏ュ娉�" /> + </el-form-item> + </el-card> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmitClick">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-dialog> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "Dict", + inherititems: false, +}); + +import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/system/dict.api"; + +import router from "@/router"; + +const queryFormRef = ref(); +const dataFormRef = ref(); + +const loading = ref(false); +const ids = ref<number[]>([]); +const total = ref(0); + +const queryParams = reactive<DictPageQuery>({ + pageNum: 1, + pageSize: 10, +}); + +const tableData = ref<DictPageVO[]>(); + +const dialog = reactive({ + title: "", + visible: false, +}); + +const formData = reactive<DictForm>({}); + +const computedRules = computed(() => { + const rules: Partial<Record<string, any>> = { + name: [{ required: true, message: "璇疯緭鍏ュ瓧鍏稿悕绉�", trigger: "blur" }], + dictCode: [{ required: true, message: "璇疯緭鍏ュ瓧鍏哥紪鐮�", trigger: "blur" }], + }; + return rules; +}); + +// 鏌ヨ +function handleQuery() { + loading.value = true; + DictAPI.getPage(queryParams) + .then((data) => { + tableData.value = data.list; + total.value = data.total; + }) + .finally(() => { + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + queryParams.pageNum = 1; + handleQuery(); +} + +// 琛岄�夋嫨 +function handleSelectionChange(selection: any) { + ids.value = selection.map((item: any) => item.id); +} + +// 鏂板瀛楀吀 +function handleAddClick() { + dialog.visible = true; + dialog.title = "鏂板瀛楀吀"; +} + +/** + * 缂栬緫瀛楀吀 + * + * @param id 瀛楀吀ID + */ +function handleEditClick(id: string) { + dialog.visible = true; + dialog.title = "淇敼瀛楀吀"; + DictAPI.getFormData(id).then((data) => { + Object.assign(formData, data); + }); +} + +// 鎻愪氦瀛楀吀琛ㄥ崟 +function handleSubmitClick() { + dataFormRef.value.validate((isValid: boolean) => { + if (isValid) { + loading.value = true; + const id = formData.id; + if (id) { + DictAPI.update(id, formData) + .then(() => { + ElMessage.success("淇敼鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } else { + DictAPI.create(formData) + .then(() => { + ElMessage.success("鏂板鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }) + .finally(() => (loading.value = false)); + } + } + }); +} + +// 鍏抽棴瀛楀吀寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + + dataFormRef.value.resetFields(); + dataFormRef.value.clearValidate(); + + formData.id = undefined; +} +/** + * 鍒犻櫎瀛楀吀 + * + * @param id 瀛楀吀ID + */ +function handleDelete(id?: string) { + const attrGroupIds = [id || ids.value].join(","); + if (!attrGroupIds) { + ElMessage.warning("璇峰嬀閫夊垹闄ら」"); + return; + } + ElMessageBox.confirm("纭鍒犻櫎宸查�変腑鐨勬暟鎹」?", "璀﹀憡", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + }).then( + () => { + DictAPI.deleteByIds(attrGroupIds).then(() => { + ElMessage.success("鍒犻櫎鎴愬姛"); + handleResetQuery(); + }); + }, + () => { + ElMessage.info("宸插彇娑堝垹闄�"); + } + ); +} + +// 鎵撳紑瀛楀吀椤� +function handleOpenDictData(row: DictPageVO) { + router.push({ + path: "/system/dict-item", + query: { dictCode: row.dictCode, title: "銆�" + row.name + "銆戝瓧鍏告暟鎹�" }, + }); +} + +onMounted(() => { + handleQuery(); +}); +</script> diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue new file mode 100644 index 0000000..7833b90 --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,530 @@ +<template> + <div class="app-container"> + <div class="search-bar"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item label="鍏抽敭瀛�" prop="keywords"> + <el-input + v-model="queryParams.keywords" + placeholder="鑿滃崟鍚嶇О" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="search" @click="handleQuery">鎼滅储</el-button> + <el-button icon="refresh" @click="handleResetQuery">閲嶇疆</el-button> + </el-form-item> + </el-form> + </div> + + <el-card shadow="never"> + <div class="mb-10px"> + <el-button + v-hasPerm="['sys:menu:add']" + type="success" + icon="plus" + @click="handleOpenDialog('0')" + > + 鏂板 + </el-button> + </div> + + <el-table + v-loading="loading" + :data="menuTableData" + highlight-current-row + row-key="id" + :tree-props="{ + children: 'children', + hasChildren: 'hasChildren', + }" + @row-click="handleRowClick" + > + <el-table-column label="鑿滃崟鍚嶇О" min-width="200"> + <template #default="scope"> + <template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')"> + <el-icon style="vertical-align: -0.15em"> + <component :is="scope.row.icon.replace('el-icon-', '')" /> + </el-icon> + </template> + <template v-else-if="scope.row.icon"> + <div :class="`i-svg:${scope.row.icon}`" /> + </template> + {{ scope.row.name }} + </template> + </el-table-column> + + <el-table-column label="绫诲瀷" align="center" width="80"> + <template #default="scope"> + <el-tag v-if="scope.row.type === MenuTypeEnum.CATALOG" type="warning">鐩綍</el-tag> + <el-tag v-if="scope.row.type === MenuTypeEnum.MENU" type="success">鑿滃崟</el-tag> + <el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">鎸夐挳</el-tag> + <el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">澶栭摼</el-tag> + </template> + </el-table-column> + <el-table-column label="璺敱鍚嶇О" align="left" width="150" prop="routeName" /> + <el-table-column label="璺敱璺緞" align="left" width="150" prop="routePath" /> + <el-table-column label="缁勪欢璺緞" align="left" width="250" prop="component" /> + <el-table-column label="鏉冮檺鏍囪瘑" align="center" width="200" prop="perm" /> + <el-table-column label="鐘舵��" align="center" width="80"> + <template #default="scope"> + <el-tag v-if="scope.row.visible === 1" type="success">鏄剧ず</el-tag> + <el-tag v-else type="info">闅愯棌</el-tag> + </template> + </el-table-column> + <el-table-column label="鎺掑簭" align="center" width="80" prop="sort" /> + <el-table-column fixed="right" align="center" label="鎿嶄綔" width="220"> + <template #default="scope"> + <el-button + v-if="scope.row.type == MenuTypeEnum.CATALOG || scope.row.type == MenuTypeEnum.MENU" + v-hasPerm="['sys:menu:add']" + type="primary" + link + size="small" + icon="plus" + @click.stop="handleOpenDialog(scope.row.id)" + > + 鏂板 + </el-button> + + <el-button + v-hasPerm="['sys:menu:edit']" + type="primary" + link + size="small" + icon="edit" + @click.stop="handleOpenDialog(undefined, scope.row.id)" + > + 缂栬緫 + </el-button> + <el-button + v-hasPerm="['sys:menu:delete']" + type="danger" + link + size="small" + icon="delete" + @click.stop="handleDelete(scope.row.id)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + </el-card> + + <el-drawer v-model="dialog.visible" :title="dialog.title" size="50%" @close="handleCloseDialog"> + <el-form ref="menuFormRef" :model="formData" :rules="rules" label-width="100px"> + <el-form-item label="鐖剁骇鑿滃崟" prop="parentId"> + <el-tree-select + v-model="formData.parentId" + placeholder="閫夋嫨涓婄骇鑿滃崟" + :data="menuOptions" + filterable + check-strictly + :render-after-expand="false" + /> + </el-form-item> + + <el-form-item label="鑿滃崟鍚嶇О" prop="name"> + <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�" /> + </el-form-item> + + <el-form-item label="鑿滃崟绫诲瀷" prop="type"> + <el-radio-group v-model="formData.type" @change="handleMenuTypeChange"> + <el-radio v-for="config in MenuTypeConfig" :key="config.value" :value="config.value"> + {{ config.label }} + </el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item v-if="formData.type == MenuTypeEnum.EXTLINK" label="澶栭摼鍦板潃" prop="path"> + <el-input v-model="formData.routePath" placeholder="璇疯緭鍏ュ閾惧畬鏁磋矾寰�" /> + </el-form-item> + + <el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="routeName"> + <template #label> + <div class="flex-y-center"> + 璺敱鍚嶇О + <el-tooltip placement="bottom" effect="light"> + <template #content> + 濡傛灉闇�瑕佸紑鍚紦瀛橈紝闇�淇濊瘉椤甸潰 defineOptions 涓殑 name 涓庢澶勪竴鑷达紝寤鸿浣跨敤椹煎嘲銆� + </template> + <el-icon class="ml-1 cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </template> + <el-input v-model="formData.routeName" placeholder="User" /> + </el-form-item> + + <el-form-item + v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU" + prop="routePath" + > + <template #label> + <div class="flex-y-center"> + 璺敱璺緞 + <el-tooltip placement="bottom" effect="light"> + <template #content> + 瀹氫箟搴旂敤涓笉鍚岄〉闈㈠搴旂殑 URL 璺緞锛岀洰褰曢渶浠� / 寮�澶达紝鑿滃崟椤逛笉鐢ㄣ�備緥濡傦細绯荤粺绠$悊鐩綍 + /system锛岀郴缁熺鐞嗕笅鐨勭敤鎴风鐞嗚彍鍗� user銆� + </template> + <el-icon class="ml-1 cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </template> + <el-input + v-if="formData.type == MenuTypeEnum.CATALOG" + v-model="formData.routePath" + placeholder="system" + /> + <el-input v-else v-model="formData.routePath" placeholder="user" /> + </el-form-item> + + <el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component"> + <template #label> + <div class="flex-y-center"> + 缁勪欢璺緞 + <el-tooltip placement="bottom" effect="light"> + <template #content> + 缁勪欢椤甸潰瀹屾暣璺緞锛岀浉瀵逛簬 src/views/锛屽 system/user/index锛岀己鐪佸悗缂� .vue + </template> + <el-icon class="ml-1 cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </template> + + <el-input v-model="formData.component" placeholder="system/user/index" style="width: 95%"> + <template v-if="formData.type == MenuTypeEnum.MENU" #prepend>src/views/</template> + <template v-if="formData.type == MenuTypeEnum.MENU" #append>.vue</template> + </el-input> + </el-form-item> + + <el-form-item v-if="formData.type == MenuTypeEnum.MENU"> + <template #label> + <div class="flex-y-center"> + 璺敱鍙傛暟 + <el-tooltip placement="bottom" effect="light"> + <template #content> + 缁勪欢椤甸潰浣跨敤 `useRoute().query.鍙傛暟鍚峘 鑾峰彇璺敱鍙傛暟鍊笺�� + </template> + <el-icon class="ml-1 cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </template> + + <div v-if="!formData.params || formData.params.length === 0"> + <el-button type="success" plain @click="formData.params = [{ key: '', value: '' }]"> + 娣诲姞璺敱鍙傛暟 + </el-button> + </div> + + <div v-else> + <div v-for="(item, index) in formData.params" :key="index"> + <el-input v-model="item.key" placeholder="鍙傛暟鍚�" style="width: 100px" /> + + <span class="mx-1">=</span> + + <el-input v-model="item.value" placeholder="鍙傛暟鍊�" style="width: 100px" /> + + <el-icon + v-if="formData.params.indexOf(item) === formData.params.length - 1" + class="ml-2 cursor-pointer color-[var(--el-color-success)]" + style="vertical-align: -0.15em" + @click="formData.params.push({ key: '', value: '' })" + > + <CirclePlusFilled /> + </el-icon> + <el-icon + class="ml-2 cursor-pointer color-[var(--el-color-danger)]" + style="vertical-align: -0.15em" + @click="formData.params.splice(formData.params.indexOf(item), 1)" + > + <DeleteFilled /> + </el-icon> + </div> + </div> + </el-form-item> + + <el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" prop="visible" label="鏄剧ず鐘舵��"> + <el-radio-group v-model="formData.visible"> + <el-radio :value="1">鏄剧ず</el-radio> + <el-radio :value="0">闅愯棌</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item + v-if="formData.type === MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU" + > + <template #label> + <div class="flex-y-center"> + 濮嬬粓鏄剧ず + <el-tooltip placement="bottom" effect="light"> + <template #content> + 閫夋嫨鈥滄槸鈥濓紝鍗充娇鐩綍鎴栬彍鍗曚笅鍙湁涓�涓瓙鑺傜偣锛屼篃浼氭樉绀虹埗鑺傜偣銆� + <br /> + 閫夋嫨鈥滃惁鈥濓紝濡傛灉鐩綍鎴栬彍鍗曚笅鍙湁涓�涓瓙鑺傜偣锛屽垯鍙樉绀鸿瀛愯妭鐐癸紝闅愯棌鐖惰妭鐐广�� + <br /> + 濡傛灉鏄彾瀛愯妭鐐癸紝璇烽�夋嫨鈥滃惁鈥濄�� + </template> + <el-icon class="ml-1 cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </template> + + <el-radio-group v-model="formData.alwaysShow"> + <el-radio :value="1">鏄�</el-radio> + <el-radio :value="0">鍚�</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item v-if="formData.type === MenuTypeEnum.MENU" label="缂撳瓨椤甸潰"> + <el-radio-group v-model="formData.keepAlive"> + <el-radio :value="1">寮�鍚�</el-radio> + <el-radio :value="0">鍏抽棴</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item label="鎺掑簭" prop="sort"> + <el-input-number + v-model="formData.sort" + style="width: 100px" + controls-position="right" + :min="0" + /> + </el-form-item> + + <!-- 鏉冮檺鏍囪瘑 --> + <el-form-item v-if="formData.type == MenuTypeEnum.BUTTON" label="鏉冮檺鏍囪瘑" prop="perm"> + <el-input v-model="formData.perm" placeholder="sys:user:add" /> + </el-form-item> + + <el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="鍥炬爣" prop="icon"> + <!-- 鍥炬爣閫夋嫨鍣� --> + <icon-select v-model="formData.icon" /> + </el-form-item> + + <el-form-item v-if="formData.type == MenuTypeEnum.CATALOG" label="璺宠浆璺敱"> + <el-input v-model="formData.redirect" placeholder="璺宠浆璺敱" /> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-drawer> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "SysMenu", + inheritAttrs: false, +}); + +import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu.api"; +import { MenuTypeEnum, MenuTypeConfig } from "@/enums/system/menu.enum"; + +const queryFormRef = ref(); +const menuFormRef = ref(); + +const loading = ref(false); +const dialog = reactive({ + title: "鏂板鑿滃崟", + visible: false, +}); + +// 鏌ヨ鍙傛暟 +const queryParams = reactive<MenuQuery>({}); +// 鑿滃崟琛ㄦ牸鏁版嵁 +const menuTableData = ref<MenuVO[]>([]); +// 椤剁骇鑿滃崟涓嬫媺閫夐」 +const menuOptions = ref<OptionType[]>([]); +// 鍒濆鑿滃崟琛ㄥ崟鏁版嵁 +const initialMenuFormData = ref<MenuForm>({ + id: undefined, + parentId: "0", + visible: 1, + sort: 1, + type: MenuTypeEnum.MENU, // 榛樿鑿滃崟 + alwaysShow: 0, + keepAlive: 1, + params: [], +}); +// 鑿滃崟琛ㄥ崟鏁版嵁 +const formData = ref({ ...initialMenuFormData.value }); +// 琛ㄥ崟楠岃瘉瑙勫垯 +const rules = reactive({ + parentId: [{ required: true, message: "璇烽�夋嫨鐖剁骇鑿滃崟", trigger: "blur" }], + name: [{ required: true, message: "璇疯緭鍏ヨ彍鍗曞悕绉�", trigger: "blur" }], + type: [{ required: true, message: "璇烽�夋嫨鑿滃崟绫诲瀷", trigger: "blur" }], + routeName: [{ required: true, message: "璇疯緭鍏ヨ矾鐢卞悕绉�", trigger: "blur" }], + routePath: [{ required: true, message: "璇疯緭鍏ヨ矾鐢辫矾寰�", trigger: "blur" }], + component: [{ required: true, message: "璇疯緭鍏ョ粍浠惰矾寰�", trigger: "blur" }], + visible: [{ required: true, message: "璇烽�夋嫨鏄剧ず鐘舵��", trigger: "change" }], +}); + +// 閫夋嫨琛ㄦ牸鐨勮鑿滃崟ID +const selectedMenuId = ref<string | undefined>(); + +// 鏌ヨ鑿滃崟 +function handleQuery() { + loading.value = true; + MenuAPI.getList(queryParams) + .then((data) => { + menuTableData.value = data; + }) + .finally(() => { + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + handleQuery(); +} + +// 琛岀偣鍑讳簨浠� +function handleRowClick(row: MenuVO) { + selectedMenuId.value = row.id; +} + +/** + * 鎵撳紑琛ㄥ崟寮圭獥 + * + * @param parentId 鐖惰彍鍗旾D + * @param menuId 鑿滃崟ID + */ +function handleOpenDialog(parentId?: string, menuId?: string) { + MenuAPI.getOptions(true) + .then((data) => { + menuOptions.value = [{ value: "0", label: "椤剁骇鑿滃崟", children: data }]; + }) + .then(() => { + dialog.visible = true; + if (menuId) { + dialog.title = "缂栬緫鑿滃崟"; + MenuAPI.getFormData(menuId).then((data) => { + initialMenuFormData.value = { ...data }; + formData.value = data; + }); + } else { + dialog.title = "鏂板鑿滃崟"; + formData.value.parentId = parentId?.toString(); + } + }); +} + +// 鑿滃崟绫诲瀷鍒囨崲 +function handleMenuTypeChange() { + // 濡傛灉鑿滃崟绫诲瀷鏀瑰彉 + if (formData.value.type !== initialMenuFormData.value.type) { + if (formData.value.type === MenuTypeEnum.MENU) { + // 鐩綍鍒囨崲鍒拌彍鍗曟椂锛屾竻绌虹粍浠惰矾寰� + if (initialMenuFormData.value.type === MenuTypeEnum.CATALOG) { + formData.value.component = ""; + } else { + // 鍏朵粬鎯呭喌锛屼繚鐣欏師鏈夌殑缁勪欢璺緞 + formData.value.routePath = initialMenuFormData.value.routePath; + formData.value.component = initialMenuFormData.value.component; + } + } + } +} + +/** + * 鎻愪氦琛ㄥ崟 + */ +function handleSubmit() { + menuFormRef.value.validate((isValid: boolean) => { + if (isValid) { + const menuId = formData.value.id; + if (menuId) { + //淇敼鏃剁埗绾ц彍鍗曚笉鑳戒负褰撳墠鑿滃崟 + if (formData.value.parentId == menuId) { + ElMessage.error("鐖剁骇鑿滃崟涓嶈兘涓哄綋鍓嶈彍鍗�"); + return; + } + MenuAPI.update(menuId, formData.value).then(() => { + ElMessage.success("淇敼鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }); + } else { + MenuAPI.create(formData.value).then(() => { + ElMessage.success("鏂板鎴愬姛"); + handleCloseDialog(); + handleQuery(); + }); + } + } + }); +} + +// 鍒犻櫎鑿滃崟 +function handleDelete(menuId: string) { + if (!menuId) { + ElMessage.warning("璇峰嬀閫夊垹闄ら」"); + return false; + } + + ElMessageBox.confirm("纭鍒犻櫎宸查�変腑鐨勬暟鎹」?", "璀﹀憡", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + }).then( + () => { + loading.value = true; + MenuAPI.deleteById(menuId) + .then(() => { + ElMessage.success("鍒犻櫎鎴愬姛"); + handleQuery(); + }) + .finally(() => { + loading.value = false; + }); + }, + () => { + ElMessage.info("宸插彇娑堝垹闄�"); + } + ); +} + +function resetForm() { + menuFormRef.value.resetFields(); + menuFormRef.value.clearValidate(); + formData.value = { + id: undefined, + parentId: "0", + visible: 1, + sort: 1, + type: MenuTypeEnum.MENU, // 榛樿鑿滃崟 + alwaysShow: 0, + keepAlive: 1, + params: [], + }; +} + +// 鍏抽棴寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + resetForm(); +} + +onMounted(() => { + handleQuery(); +}); +</script> diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue new file mode 100644 index 0000000..00b2e58 --- /dev/null +++ b/src/views/system/role/index.vue @@ -0,0 +1,451 @@ +<template> + <div class="app-container"> + <div class="search-bar"> + <el-form ref="queryFormRef" :model="queryParams" :inline="true"> + <el-form-item prop="keywords" label="鍏抽敭瀛�"> + <el-input + v-model="queryParams.keywords" + placeholder="瑙掕壊鍚嶇О" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + + <el-form-item> + <el-button type="primary" icon="search" @click="handleQuery">鎼滅储</el-button> + <el-button icon="refresh" @click="handleResetQuery">閲嶇疆</el-button> + </el-form-item> + </el-form> + </div> + + <el-card shadow="never"> + <div class="mb-10px"> + <el-button type="success" icon="plus" @click="handleOpenDialog()">鏂板</el-button> + <el-button type="danger" :disabled="ids.length === 0" icon="delete" @click="handleDelete()"> + 鍒犻櫎 + </el-button> + </div> + + <el-table + ref="dataTableRef" + v-loading="loading" + :data="roleList" + highlight-current-row + border + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" align="center" /> + <el-table-column label="瑙掕壊鍚嶇О" prop="name" min-width="100" /> + <el-table-column label="瑙掕壊缂栫爜" prop="code" width="150" /> + + <el-table-column label="鐘舵��" align="center" width="100"> + <template #default="scope"> + <el-tag v-if="scope.row.status === 1" type="success">姝e父</el-tag> + <el-tag v-else type="info">绂佺敤</el-tag> + </template> + </el-table-column> + + <el-table-column label="鎺掑簭" align="center" width="80" prop="sort" /> + + <el-table-column fixed="right" label="鎿嶄綔" width="220"> + <template #default="scope"> + <el-button + type="primary" + size="small" + link + icon="position" + @click="handleOpenAssignPermDialog(scope.row)" + > + 鍒嗛厤鏉冮檺 + </el-button> + <el-button + type="primary" + size="small" + link + icon="edit" + @click="handleOpenDialog(scope.row.id)" + > + 缂栬緫 + </el-button> + <el-button + type="danger" + size="small" + link + icon="delete" + @click="handleDelete(scope.row.id)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-if="total > 0" + v-model:total="total" + v-model:page="queryParams.pageNum" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </el-card> + + <!-- 瑙掕壊琛ㄥ崟寮圭獥 --> + <el-dialog + v-model="dialog.visible" + :title="dialog.title" + width="500px" + @close="handleCloseDialog" + > + <el-form ref="roleFormRef" :model="formData" :rules="rules" label-width="100px"> + <el-form-item label="瑙掕壊鍚嶇О" prop="name"> + <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ鑹插悕绉�" /> + </el-form-item> + + <el-form-item label="瑙掕壊缂栫爜" prop="code"> + <el-input v-model="formData.code" placeholder="璇疯緭鍏ヨ鑹茬紪鐮�" /> + </el-form-item> + + <el-form-item label="鏁版嵁鏉冮檺" prop="dataScope"> + <el-select v-model="formData.dataScope"> + <el-option :key="1" label="鍏ㄩ儴鏁版嵁" :value="1" /> + <el-option :key="2" label="閮ㄩ棬鍙婂瓙閮ㄩ棬鏁版嵁" :value="2" /> + <el-option :key="3" label="鏈儴闂ㄦ暟鎹�" :value="3" /> + <el-option :key="4" label="鏈汉鏁版嵁" :value="4" /> + </el-select> + </el-form-item> + + <el-form-item label="鐘舵��" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio :value="1">姝e父</el-radio> + <el-radio :value="0">鍋滅敤</el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item label="鎺掑簭" prop="sort"> + <el-input-number + v-model="formData.sort" + controls-position="right" + :min="0" + style="width: 100px" + /> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-dialog> + + <!-- 鍒嗛厤鏉冮檺寮圭獥 --> + <el-drawer + v-model="assignPermDialogVisible" + :title="'銆�' + checkedRole.name + '銆戞潈闄愬垎閰�'" + size="500" + > + <div class="flex-x-between"> + <el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="鑿滃崟鏉冮檺鍚嶇О"> + <template #prefix> + <Search /> + </template> + </el-input> + + <div class="flex-center ml-5"> + <el-button type="primary" size="small" plain @click="togglePermTree"> + <template #icon> + <Switch /> + </template> + {{ isExpanded ? "鏀剁缉" : "灞曞紑" }} + </el-button> + <el-checkbox + v-model="parentChildLinked" + class="ml-5" + @change="handleparentChildLinkedChange" + > + 鐖跺瓙鑱斿姩 + </el-checkbox> + + <el-tooltip placement="bottom"> + <template #content> + 濡傛灉鍙渶鍕鹃�夎彍鍗曟潈闄愶紝涓嶉渶瑕佸嬀閫夊瓙鑿滃崟鎴栬�呮寜閽潈闄愶紝璇峰叧闂埗瀛愯仈鍔� + </template> + <el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer"> + <QuestionFilled /> + </el-icon> + </el-tooltip> + </div> + </div> + + <el-tree + ref="permTreeRef" + node-key="value" + show-checkbox + :data="menuPermOptions" + :filter-node-method="handlePermFilter" + :default-expand-all="true" + :check-strictly="!parentChildLinked" + class="mt-5" + > + <template #default="{ data }"> + {{ data.label }} + </template> + </el-tree> + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleAssignPermSubmit">纭� 瀹�</el-button> + <el-button @click="assignPermDialogVisible = false">鍙� 娑�</el-button> + </div> + </template> + </el-drawer> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: "Role", + inheritAttrs: false, +}); + +import RoleAPI, { RolePageVO, RoleForm, RolePageQuery } from "@/api/system/role.api"; +import MenuAPI from "@/api/system/menu.api"; + +const queryFormRef = ref(); +const roleFormRef = ref(); +const permTreeRef = ref(); + +const loading = ref(false); +const ids = ref<number[]>([]); +const total = ref(0); + +const queryParams = reactive<RolePageQuery>({ + pageNum: 1, + pageSize: 10, +}); + +// 瑙掕壊琛ㄦ牸鏁版嵁 +const roleList = ref<RolePageVO[]>(); +// 鑿滃崟鏉冮檺涓嬫媺 +const menuPermOptions = ref<OptionType[]>([]); + +// 寮圭獥 +const dialog = reactive({ + title: "", + visible: false, +}); +// 瑙掕壊琛ㄥ崟 +const formData = reactive<RoleForm>({ + sort: 1, + status: 1, +}); + +const rules = reactive({ + name: [{ required: true, message: "璇疯緭鍏ヨ鑹插悕绉�", trigger: "blur" }], + code: [{ required: true, message: "璇疯緭鍏ヨ鑹茬紪鐮�", trigger: "blur" }], + dataScope: [{ required: true, message: "璇烽�夋嫨鏁版嵁鏉冮檺", trigger: "blur" }], + status: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "blur" }], +}); + +// 閫変腑鐨勮鑹� +interface CheckedRole { + id?: string; + name?: string; +} +const checkedRole = ref<CheckedRole>({}); +const assignPermDialogVisible = ref(false); + +const permKeywords = ref(""); +const isExpanded = ref(true); + +const parentChildLinked = ref(true); + +// 鏌ヨ +function handleQuery() { + loading.value = true; + RoleAPI.getPage(queryParams) + .then((data) => { + roleList.value = data.list; + total.value = data.total; + }) + .finally(() => { + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + queryParams.pageNum = 1; + handleQuery(); +} + +// 琛屽閫夋閫変腑 +function handleSelectionChange(selection: any) { + ids.value = selection.map((item: any) => item.id); +} + +// 鎵撳紑瑙掕壊寮圭獥 +function handleOpenDialog(roleId?: string) { + dialog.visible = true; + if (roleId) { + dialog.title = "淇敼瑙掕壊"; + RoleAPI.getFormData(roleId).then((data) => { + Object.assign(formData, data); + }); + } else { + dialog.title = "鏂板瑙掕壊"; + } +} + +// 鎻愪氦瑙掕壊琛ㄥ崟 +function handleSubmit() { + roleFormRef.value.validate((valid: any) => { + if (valid) { + loading.value = true; + const roleId = formData.id; + if (roleId) { + RoleAPI.update(roleId, formData) + .then(() => { + ElMessage.success("淇敼鎴愬姛"); + handleCloseDialog(); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + } else { + RoleAPI.create(formData) + .then(() => { + ElMessage.success("鏂板鎴愬姛"); + handleCloseDialog(); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + } + } + }); +} + +// 鍏抽棴寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + + roleFormRef.value.resetFields(); + roleFormRef.value.clearValidate(); + + formData.id = undefined; + formData.sort = 1; + formData.status = 1; +} + +// 鍒犻櫎瑙掕壊 +function handleDelete(roleId?: string) { + const roleIds = [roleId || ids.value].join(","); + if (!roleIds) { + ElMessage.warning("璇峰嬀閫夊垹闄ら」"); + return; + } + + ElMessageBox.confirm("纭鍒犻櫎宸查�変腑鐨勬暟鎹」?", "璀﹀憡", { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning", + }).then( + () => { + loading.value = true; + RoleAPI.deleteByIds(roleIds) + .then(() => { + ElMessage.success("鍒犻櫎鎴愬姛"); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + }, + () => { + ElMessage.info("宸插彇娑堝垹闄�"); + } + ); +} + +// 鎵撳紑鍒嗛厤鑿滃崟鏉冮檺寮圭獥 +async function handleOpenAssignPermDialog(row: RolePageVO) { + const roleId = row.id; + if (roleId) { + assignPermDialogVisible.value = true; + loading.value = true; + + checkedRole.value.id = roleId; + checkedRole.value.name = row.name; + + // 鑾峰彇鎵�鏈夌殑鑿滃崟 + menuPermOptions.value = await MenuAPI.getOptions(); + + // 鍥炴樉瑙掕壊宸叉嫢鏈夌殑鑿滃崟 + RoleAPI.getRoleMenuIds(roleId) + .then((data) => { + const checkedMenuIds = data; + checkedMenuIds.forEach((menuId) => permTreeRef.value!.setChecked(menuId, true, false)); + }) + .finally(() => { + loading.value = false; + }); + } +} + +// 鍒嗛厤鑿滃崟鏉冮檺鎻愪氦 +function handleAssignPermSubmit() { + const roleId = checkedRole.value.id; + if (roleId) { + const checkedMenuIds: number[] = permTreeRef + .value!.getCheckedNodes(false, true) + .map((node: any) => node.value); + + loading.value = true; + RoleAPI.updateRoleMenus(roleId, checkedMenuIds) + .then(() => { + ElMessage.success("鍒嗛厤鏉冮檺鎴愬姛"); + assignPermDialogVisible.value = false; + handleResetQuery(); + }) + .finally(() => { + loading.value = false; + }); + } +} + +// 灞曞紑/鏀剁缉 鑿滃崟鏉冮檺鏍� +function togglePermTree() { + isExpanded.value = !isExpanded.value; + if (permTreeRef.value) { + Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => { + if (isExpanded.value) { + node.expand(); + } else { + node.collapse(); + } + }); + } +} + +// 鏉冮檺绛涢�� +watch(permKeywords, (val) => { + permTreeRef.value!.filter(val); +}); + +function handlePermFilter( + value: string, + data: { + [key: string]: any; + } +) { + if (!value) return true; + return data.label.includes(value); +} + +// 鐖跺瓙鑿滃崟鑺傜偣鏄惁鑱斿姩 +function handleparentChildLinkedChange(val: any) { + parentChildLinked.value = val; +} + +onMounted(() => { + handleQuery(); +}); +</script> diff --git a/src/views/system/user/components/DeptTree.vue b/src/views/system/user/components/DeptTree.vue new file mode 100644 index 0000000..fca2307 --- /dev/null +++ b/src/views/system/user/components/DeptTree.vue @@ -0,0 +1,70 @@ +<!-- 閮ㄩ棬鏍� --> +<template> + <el-card shadow="never"> + <el-input v-model="deptName" placeholder="閮ㄩ棬鍚嶇О" clearable> + <template #prefix> + <el-icon><Search /></el-icon> + </template> + </el-input> + + <el-tree + ref="deptTreeRef" + class="mt-2" + :data="deptList" + :props="{ children: 'children', label: 'label', disabled: '' }" + :expand-on-click-node="false" + :filter-node-method="handleFilter" + default-expand-all + @node-click="handleNodeClick" + /> + </el-card> +</template> + +<script setup lang="ts"> +import DeptAPI from "@/api/system/dept.api"; +const props = defineProps({ + modelValue: { + type: [String, Number], + default: undefined, + }, +}); + +const deptList = ref<OptionType[]>(); // 閮ㄩ棬鍒楄〃 +const deptTreeRef = ref(); // 閮ㄩ棬鏍� +const deptName = ref(); // 閮ㄩ棬鍚嶇О + +const emits = defineEmits(["node-click"]); + +const deptId = useVModel(props, "modelValue", emits); + +watchEffect( + () => { + deptTreeRef.value.filter(deptName.value); + }, + { + flush: "post", // watchEffect浼氬湪DOM鎸傝浇鎴栬�呮洿鏂颁箣鍓嶅氨浼氳Е鍙戯紝姝ゅ睘鎬ф帶鍒跺湪DOM鍏冪礌鏇存柊鍚庤繍琛� + } +); + +/** + * 閮ㄩ棬绛涢�� + */ +function handleFilter(value: string, data: any) { + if (!value) { + return true; + } + return data.label.indexOf(value) !== -1; +} + +/** 閮ㄩ棬鏍戣妭鐐� Click */ +function handleNodeClick(data: { [key: string]: any }) { + deptId.value = data.value; + emits("node-click"); +} + +onBeforeMount(() => { + DeptAPI.getOptions().then((data) => { + deptList.value = data; + }); +}); +</script> diff --git a/src/views/system/user/components/UserImport.vue b/src/views/system/user/components/UserImport.vue new file mode 100644 index 0000000..4fdd63b --- /dev/null +++ b/src/views/system/user/components/UserImport.vue @@ -0,0 +1,199 @@ +<template> + <div> + <el-dialog + v-model="visible" + :align-center="true" + title="瀵煎叆鏁版嵁" + width="600px" + @close="handleClose" + > + <el-scrollbar max-height="60vh"> + <el-form + ref="importFormRef" + label-width="auto" + style="padding-right: var(--el-dialog-padding-primary)" + :model="importFormData" + :rules="importFormRules" + > + <el-form-item label="鏂囦欢鍚�" prop="files"> + <el-upload + ref="uploadRef" + v-model:file-list="importFormData.files" + class="w-full" + accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + :drag="true" + :limit="1" + :auto-upload="false" + :on-exceed="handleFileExceed" + > + <el-icon class="el-icon--upload"><upload-filled /></el-icon> + <div class="el-upload__text"> + 灏嗘枃浠舵嫋鍒版澶勶紝鎴� + <em>鐐瑰嚮涓婁紶</em> + </div> + <template #tip> + <div class="el-upload__tip"> + 鏍煎紡涓�*.xlsx / *.xls锛屾枃浠朵笉瓒呰繃涓�涓� + <el-link + type="primary" + icon="download" + :underline="false" + @click="handleDownloadTemplate" + > + 涓嬭浇妯℃澘 + </el-link> + </div> + </template> + </el-upload> + </el-form-item> + </el-form> + </el-scrollbar> + <template #footer> + <div style="padding-right: var(--el-dialog-padding-primary)"> + <el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult"> + 閿欒淇℃伅 + </el-button> + <el-button + type="primary" + :disabled="importFormData.files.length === 0" + @click="handleUpload" + > + 纭� 瀹� + </el-button> + <el-button @click="handleClose">鍙� 娑�</el-button> + </div> + </template> + </el-dialog> + + <el-dialog v-model="resultVisible" title="瀵煎叆缁撴灉" width="600px"> + <el-alert + :title="`瀵煎叆缁撴灉锛�${invalidCount}鏉℃棤鏁堟暟鎹紝${validCount}鏉℃湁鏁堟暟鎹甡" + type="warning" + :closable="false" + /> + <el-table :data="resultData" style="width: 100%; max-height: 400px"> + <el-table-column prop="index" align="center" width="100" type="index" label="搴忓彿" /> + <el-table-column prop="message" label="閿欒淇℃伅" width="400"> + <template #default="scope"> + {{ scope.row }} + </template> + </el-table-column> + </el-table> + <template #footer> + <div class="dialog-footer"> + <el-button @click="handleCloseResult">鍏抽棴</el-button> + </div> + </template> + </el-dialog> + </div> +</template> + +<script lang="ts" setup> +import { ElMessage, type UploadUserFile } from "element-plus"; +import UserAPI from "@/api/system/user.api"; +import { ResultCode } from "@/enums/common/result.enum"; + +const emit = defineEmits(["import-success"]); +const visible = defineModel("modelValue", { + type: Boolean, + required: true, + default: false, +}); + +const resultVisible = ref(false); +const resultData = ref<string[]>([]); +const invalidCount = ref(0); +const validCount = ref(0); + +const importFormRef = ref(null); +const uploadRef = ref(null); + +const importFormData = reactive<{ + files: UploadUserFile[]; +}>({ + files: [], +}); + +watch(visible, (newValue) => { + if (newValue) { + resultData.value = []; + resultVisible.value = false; + invalidCount.value = 0; + validCount.value = 0; + } +}); + +const importFormRules = { + files: [{ required: true, message: "鏂囦欢涓嶈兘涓虹┖", trigger: "blur" }], +}; + +// 鏂囦欢瓒呭嚭涓暟闄愬埗 +const handleFileExceed = () => { + ElMessage.warning("鍙兘涓婁紶涓�涓枃浠�"); +}; + +// 涓嬭浇瀵煎叆妯℃澘 +const handleDownloadTemplate = () => { + UserAPI.downloadTemplate().then((response: any) => { + const fileData = response.data; + const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]); + const fileType = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; + + const blob = new Blob([fileData], { type: fileType }); + const downloadUrl = window.URL.createObjectURL(blob); + + const downloadLink = document.createElement("a"); + downloadLink.href = downloadUrl; + downloadLink.download = fileName; + + document.body.appendChild(downloadLink); + downloadLink.click(); + + document.body.removeChild(downloadLink); + window.URL.revokeObjectURL(downloadUrl); + }); +}; + +// 涓婁紶鏂囦欢 +const handleUpload = async () => { + if (!importFormData.files.length) { + ElMessage.warning("璇烽�夋嫨鏂囦欢"); + return; + } + + try { + const result = await UserAPI.import(importFormData.files[0].raw as File); + if (result.code === ResultCode.SUCCESS && result.invalidCount === 0) { + ElMessage.success("瀵煎叆鎴愬姛锛屽鍏ユ暟鎹細" + result.validCount + "鏉�"); + emit("import-success"); + handleClose(); + } else { + ElMessage.error("涓婁紶澶辫触"); + resultVisible.value = true; + resultData.value = result.messageList; + invalidCount.value = result.invalidCount; + validCount.value = result.validCount; + } + } catch (error: any) { + console.error(error); + ElMessage.error("涓婁紶澶辫触锛�" + error); + } +}; + +// 鏄剧ず閿欒淇℃伅 +const handleShowResult = () => { + resultVisible.value = true; +}; + +// 鍏抽棴閿欒淇℃伅寮圭獥 +const handleCloseResult = () => { + resultVisible.value = false; +}; + +// 鍏抽棴寮圭獥 +const handleClose = () => { + importFormData.files.length = 0; + visible.value = false; +}; +</script> diff --git a/src/views/system/user/components/addUser.vue b/src/views/system/user/components/addUser.vue new file mode 100644 index 0000000..bb2c44f --- /dev/null +++ b/src/views/system/user/components/addUser.vue @@ -0,0 +1,173 @@ +<script setup lang="ts"> +import { ElLoading, FormInstance } from "element-plus"; +import UserApi, { UserInfo } from "@/api/user"; +import { sexList, roleList } from "@/assets/const/const_user"; +import formatPassword from "@/utils/formatPassword"; + +const emit = defineEmits(["update:close", "success"]); +const formRef = ref(); +const paramsRef = ref<UserInfo>({ + id: 0, + name: "", + pwd: "", + sex: 1, + roleId: 1, + email: "", + phoneNumber: "", + createTime: "2025-04-25 00:00:00", + roles: [], + perms: [], +}); +const rulesRef = ref({ + phoneNumber: [ + { required: true, message: "璇疯緭鍏ユ墜鏈哄彿", trigger: "blur" }, + { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", + trigger: "blur", + }, + ], + email: [ + { required: true, message: "璇疯緭鍏ラ偖绠卞湴鍧�", trigger: "blur" }, + { + pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, + message: "璇疯緭鍏ユ纭殑閭鍦板潃", + trigger: "blur", + }, + ], + name: [{ required: true, message: "璇疯緭鍏ョ敤鎴峰悕绉�", trigger: "blur" }], + pwd: [{ required: true, message: "璇疯緭鍏ュ瘑鐮�", trigger: "blur" }], +}); +const layout = reactive({ + gutter: 16, + span: 24, +}); + +// 鎻愪氦琛ㄥ崟 +function submitForm(formEl: FormInstance) { + if (!formEl) return; + formEl.validate((valid, fields) => { + if (valid) { + addUser(); + } else { + ElMessage({ + message: "瀛樺湪涓嶅悎娉曟暟鎹紝璇蜂慨鏀癸紒锛侊紒", + type: "warning", + }); + console.log("error submit!", fields); + } + }); +} + +function addUser() { + const loading = ElLoading.service({ + lock: true, + text: "璁剧疆涓�...", + background: "rgba(0, 0, 0, 0.7)", + }); + const params = JSON.parse(JSON.stringify(paramsRef.value)); + params.pwd = formatPassword(params.pwd); + UserApi.add(params) + .then((res) => { + if (res.code === 1) { + ElMessage({ + message: "娣诲姞鎴愬姛锛侊紒锛�", + type: "success", + }); + emit("success", true); + emit("update:close", false); + } else { + ElMessage({ + message: "娣诲姞澶辫触锛侊紒锛�", + type: "warning", + }); + } + }) + .catch((error) => { + console.log(error); + ElMessage({ + message: "鎺ュ彛璇锋眰澶辫触锛岃鑱旂郴绠$悊鍛橈紒锛侊紒", + type: "error", + }); + }) + .finally(() => { + loading.close(); + }); +} + +function close() { + emit("update:close", false); +} +</script> + +<template> + <div class="dialog-wrapper"> + <el-form + ref="formRef" + label-position="top" + :model="paramsRef" + :rules="rulesRef" + label-width="auto" + > + <el-row :gutter="layout.gutter"> + <el-col :span="layout.span"> + <el-form-item label="鐢ㄦ埛鍚嶇О" prop="name"> + <el-input v-model="paramsRef.name"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="瀵嗙爜" prop="pwd"> + <el-input v-model="paramsRef.pwd"></el-input> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="鎬у埆" prop="sex"> + <el-select v-model="paramsRef.sex"> + <el-option + v-for="item in sexList" + :key="item.value" + :label="item.label" + :value="item.value" + ></el-option> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="瑙掕壊" prop="roleId"> + <el-select v-model="paramsRef.roleId"> + <el-option + v-for="item in roleList" + :key="item.value" + :label="item.label" + :value="item.value" + ></el-option> + </el-select> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="閭" prop="email"> + <el-input v-model="paramsRef.email"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="鐢佃瘽鍙风爜" prop="phoneNumber"> + <el-input v-model="paramsRef.phoneNumber"></el-input> + </el-form-item> + </el-col> + </el-row> + <div class="from-footer"> + <el-button type="primary" @click="submitForm(formRef)">纭畾</el-button> + <el-button type="danger" @click="close">鍙栨秷</el-button> + </div> + </el-form> + </div> +</template> + +<style scoped lang="scss"> +.dialog-wrapper { + width: 580px; + .from-footer { + text-align: right; + } +} +</style> diff --git a/src/views/system/user/components/editUser.vue b/src/views/system/user/components/editUser.vue new file mode 100644 index 0000000..bb70329 --- /dev/null +++ b/src/views/system/user/components/editUser.vue @@ -0,0 +1,174 @@ +<script setup lang="ts"> +import { ElLoading, FormInstance } from "element-plus"; +import UserApi, { UserInfo } from "@/api/user"; +import { sexList, roleList } from "@/assets/const/const_user"; +import { useUserStore } from "@/store"; + +const { userInfo } = useUserStore(); + +const props = defineProps(["data"]); +const emit = defineEmits(["update:close", "success"]); +const formRef = ref(); +const paramsRef = ref<UserInfo>({ + id: 0, + name: "", + pwd: "123456", + sex: 1, + roleId: 1, + email: "496960745@qq.com", + phoneNumber: "18171444936", + createTime: "2025-04-25 00:00:00", + roles: [], + perms: [], +}); +const rulesRef = ref({ + phoneNumber: [ + { required: true, message: "璇疯緭鍏ユ墜鏈哄彿", trigger: "blur" }, + { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", + trigger: "blur", + }, + ], + email: [ + { required: true, message: "璇疯緭鍏ラ偖绠卞湴鍧�", trigger: "blur" }, + { + pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, + message: "璇疯緭鍏ユ纭殑閭鍦板潃", + trigger: "blur", + }, + ], + name: [{ required: true, message: "璇疯緭鍏ョ敤鎴峰悕绉�", trigger: "blur" }], + pwd: [{ required: true, message: "璇疯緭鍏ュ瘑鐮�", trigger: "blur" }], +}); +const layout = reactive({ + gutter: 16, + span: 24, +}); + +// 鎻愪氦琛ㄥ崟 +function submitForm(formEl: FormInstance) { + if (!formEl) return; + formEl.validate((valid, fields) => { + if (valid) { + editUser(); + } else { + ElMessage({ + message: "瀛樺湪涓嶅悎娉曟暟鎹紝璇蜂慨鏀癸紒锛侊紒", + type: "warning", + }); + console.log("error submit!", fields); + } + }); +} + +function editUser() { + const loading = ElLoading.service({ + lock: true, + text: "璁剧疆涓�...", + background: "rgba(0, 0, 0, 0.7)", + }); + const params = JSON.parse(JSON.stringify(paramsRef.value)); + UserApi.edit(params) + .then((res) => { + if (res.code === 1) { + ElMessage({ + message: "淇敼鎴愬姛锛侊紒锛�", + type: "success", + }); + emit("success", true); + emit("update:close", false); + } else { + ElMessage({ + message: "淇敼澶辫触锛侊紒锛�", + type: "warning", + }); + } + }) + .catch((error) => { + console.log(error); + ElMessage({ + message: "鎺ュ彛璇锋眰澶辫触锛岃鑱旂郴绠$悊鍛橈紒锛侊紒", + type: "error", + }); + }) + .finally(() => { + loading.close(); + }); +} + +function close() { + emit("update:close", false); +} + +onMounted(() => { + paramsRef.value = JSON.parse(JSON.stringify(props.data)); +}); +</script> + +<template> + <div class="dialog-wrapper"> + <el-form + ref="formRef" + label-position="top" + :model="paramsRef" + :rules="rulesRef" + label-width="auto" + > + <el-row :gutter="layout.gutter"> + <el-col :span="layout.span"> + <el-form-item label="鐢ㄦ埛鍚嶇О" prop="name"> + <el-input v-model="paramsRef.name"></el-input> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="鎬у埆" prop="sex"> + <el-select v-model="paramsRef.sex"> + <el-option + v-for="item in sexList" + :key="item.value" + :label="item.label" + :value="item.value" + ></el-option> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="瑙掕壊" prop="roleId"> + <el-select v-model="paramsRef.roleId" :disabled="userInfo.name === paramsRef.name"> + <el-option + v-for="item in roleList" + :key="item.value" + :label="item.label" + :value="item.value" + ></el-option> + </el-select> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="閭" prop="email"> + <el-input v-model="paramsRef.email"></el-input> + </el-form-item> + </el-col> + <el-col :span="layout.span"> + <el-form-item label="鐢佃瘽鍙风爜" prop="phoneNumber"> + <el-input v-model="paramsRef.phoneNumber"></el-input> + </el-form-item> + </el-col> + </el-row> + <div class="from-footer"> + <el-button type="primary" @click="submitForm(formRef)">纭畾</el-button> + <el-button type="danger" @click="close">鍙栨秷</el-button> + </div> + </el-form> + </div> +</template> + +<style scoped lang="scss"> +.dialog-wrapper { + width: 580px; + .from-footer { + text-align: right; + } +} +</style> diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 0000000..8cc7bc9 --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,302 @@ +<!-- 鐢ㄦ埛绠$悊 --> +<template> + <div class="app-container"> + <el-row :gutter="20"> + <!-- 鐢ㄦ埛鍒楄〃 --> + <el-col :lg="24" :xs="24"> + <el-card shadow="never"> + <div class="flex-x-between mb-10px"> + <div> + <el-button + v-hasPerm="['sys:user:add']" + type="success" + icon="plus" + @click="handleOpenDialog()" + > + 鏂板 + </el-button> + </div> + </div> + <el-table v-loading="loading" :data="pageData" @selection-change="handleSelectionChange"> + <el-table-column type="selection" width="50" align="center" /> + <el-table-column label="鐢ㄦ埛鍚�" width="100" prop="name" align="center" /> + <el-table-column label="鎬у埆" width="100" align="center"> + <template #default="scope"> + <el-tag :type="scope.row.sex === 1 ? 'success' : 'primary'"> + {{ scope.row.sex === 1 ? "鐢�" : "濂�" }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="瑙掕壊" width="100" align="center"> + <template #default="scope"> + <el-tag :type="scope.row.roleId === 1 ? 'success' : 'primary'"> + {{ scope.row.roleId === 1 ? "瀹㈡埛" : "鍚庡彴绠$悊" }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎵嬫満鍙风爜" align="center" prop="phoneNumber" /> + <el-table-column label="閭" align="center" prop="email" /> + <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180" /> + <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220"> + <template #default="{ row }"> + <el-button + v-hasPerm="'sys:user:edit'" + type="primary" + icon="edit" + link + size="small" + @click="handleOpenEditDialog(row)" + > + 缂栬緫 + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-if="total > 0" + v-model:total="total" + v-model:page="queryParams.pageNum" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </el-card> + </el-col> + </el-row> + + <!-- 鐢ㄦ埛琛ㄥ崟 --> + <el-drawer + v-model="dialog.visible" + :title="dialog.title" + append-to-body + @close="handleCloseDialog" + > + <el-form ref="userFormRef" :model="formData" :rules="rules" label-width="80px"> + <el-form-item label="鐢ㄦ埛鍚�" prop="username"> + <el-input + v-model="formData.username" + :readonly="!!formData.id" + 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="deptId"> + <el-tree-select + v-model="formData.deptId" + placeholder="璇烽�夋嫨鎵�灞為儴闂�" + :data="deptOptions" + filterable + check-strictly + :render-after-expand="false" + /> + </el-form-item> + + <el-form-item label="鎬у埆" prop="gender"> + <Dict v-model="formData.gender" code="gender" /> + </el-form-item> + + <el-form-item label="瑙掕壊" prop="roleIds"> + <el-select v-model="formData.roleIds" multiple placeholder="璇烽�夋嫨"> + <el-option + v-for="item in roleOptions" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + + <el-form-item label="鎵嬫満鍙风爜" prop="mobile"> + <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" maxlength="11" /> + </el-form-item> + + <el-form-item label="閭" prop="email"> + <el-input v-model="formData.email" placeholder="璇疯緭鍏ラ偖绠�" maxlength="50" /> + </el-form-item> + + <el-form-item label="鐘舵��" prop="status"> + <el-switch + v-model="formData.status" + inline-prompt + active-text="姝e父" + inactive-text="绂佺敤" + :active-value="1" + :inactive-value="0" + /> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button> + <el-button @click="handleCloseDialog">鍙� 娑�</el-button> + </div> + </template> + </el-drawer> + + <el-dialog v-model="addDialog" draggable title="娣诲姞鐢ㄦ埛淇℃伅" align-center width="auto"> + <add-user v-if="addDialog" v-model:close="addDialog" @success="handleQuery"></add-user> + </el-dialog> + <el-dialog v-model="editDialog" draggable title="缂栬緫鐢ㄦ埛淇℃伅" align-center width="auto"> + <edit-user + v-if="editDialog" + v-model:close="editDialog" + :data="rowRef" + @success="handleQuery" + ></edit-user> + </el-dialog> + </div> +</template> + +<script setup lang="ts"> +import UserAPI, { UserForm, UserPageQuery } from "@/api/system/user.api"; +import UserApi, { UserInfo } from "@/api/user"; +import AddUser from "@/views/system/user/components/addUser.vue"; +import EditUser from "@/views/system/user/components/editUser.vue"; + +defineOptions({ + name: "User", + inheritAttrs: false, +}); +const queryFormRef = ref(); +const userFormRef = ref(); + +const queryParams = reactive<UserPageQuery>({ + pageNum: 1, + pageSize: 20, +}); + +const pageData = ref<UserInfo[]>(); +const total = ref(0); +const loading = ref(false); + +const dialog = reactive({ + visible: false, + title: "鏂板鐢ㄦ埛", +}); + +const formData = reactive<UserForm>({ + status: 1, +}); + +const rules = reactive({ + username: [{ required: true, message: "鐢ㄦ埛鍚嶄笉鑳戒负绌�", trigger: "blur" }], + nickname: [{ required: true, message: "鐢ㄦ埛鏄电О涓嶈兘涓虹┖", trigger: "blur" }], + deptId: [{ required: true, message: "鎵�灞為儴闂ㄤ笉鑳戒负绌�", trigger: "blur" }], + roleIds: [{ required: true, message: "鐢ㄦ埛瑙掕壊涓嶈兘涓虹┖", trigger: "blur" }], + email: [ + { + pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, + message: "璇疯緭鍏ユ纭殑閭鍦板潃", + trigger: "blur", + }, + ], + mobile: [ + { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", + trigger: "blur", + }, + ], +}); + +// 閫変腑鐨勭敤鎴稩D +const selectIds = ref<number[]>([]); +// 閮ㄩ棬涓嬫媺鏁版嵁婧� +const deptOptions = ref<OptionType[]>(); +// 瑙掕壊涓嬫媺鏁版嵁婧� +const roleOptions = ref<OptionType[]>(); + +// 鏌ヨ +function handleQuery() { + loading.value = true; + UserApi.search(queryParams.pageNum, queryParams.pageSize) + .then((result) => { + const { data } = result; + if (data.code == 1) { + const rs = data.data; + pageData.value = rs.list; + total.value = rs.total; + } else { + total.value = 0; + queryParams.pageNum = 0; + queryParams.pageSize = 20; + } + }) + .finally(() => { + loading.value = false; + }); +} + +// 閲嶇疆鏌ヨ +function handleResetQuery() { + queryFormRef.value.resetFields(); + queryParams.pageNum = 1; + queryParams.deptId = undefined; + queryParams.createTime = undefined; + handleQuery(); +} + +// 閫変腑椤瑰彂鐢熷彉鍖� +function handleSelectionChange(selection: any[]) { + selectIds.value = selection.map((item) => item.id); +} + + + +// 鍏抽棴寮圭獥 +function handleCloseDialog() { + dialog.visible = false; + userFormRef.value.resetFields(); + userFormRef.value.clearValidate(); + + formData.id = undefined; + formData.status = 1; +} + +// 鎻愪氦鐢ㄦ埛琛ㄥ崟锛堥槻鎶栵級 +const handleSubmit = useDebounceFn(() => { + userFormRef.value.validate((valid: boolean) => { + if (valid) { + const userId = formData.id; + loading.value = true; + if (userId) { + UserAPI.update(userId, formData) + .then(() => { + ElMessage.success("淇敼鐢ㄦ埛鎴愬姛"); + handleCloseDialog(); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + } else { + UserAPI.create(formData) + .then(() => { + ElMessage.success("鏂板鐢ㄦ埛鎴愬姛"); + handleCloseDialog(); + handleResetQuery(); + }) + .finally(() => (loading.value = false)); + } + } + }); +}, 1000); + +const addDialog = ref(false); +function handleOpenDialog() { + addDialog.value = true; +} + +const editDialog = ref(false); +const rowRef = ref<UserInfo>(); +function handleOpenEditDialog(data: UserInfo) { + rowRef.value = data; + editDialog.value = true; +} +onMounted(() => { + handleQuery(); +}); +</script> diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7afb4fc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "lib": ["esnext", "dom"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + // 涓ユ牸鎬у拰绫诲瀷妫�鏌ョ浉鍏抽厤缃� + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // 妯″潡鍜屽吋瀹规�х浉鍏抽厤缃� + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + // 璋冭瘯鍜屽吋瀹规�х浉鍏抽厤缃� + "sourceMap": true, + "useDefineForClassFields": true, + "allowJs": true, + + // 绫诲瀷澹版槑鐩稿叧閰嶇疆 + "types": ["node", "vite/client", "element-plus/global"] + }, + + "include": ["mock/**/*.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..e08e746 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,77 @@ +// https://unocss.nodejs.cn/guide/config-file +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + presetWebFonts, + transformerDirectives, + transformerVariantGroup, +} from "unocss"; + +import { FileSystemIconLoader } from "@iconify/utils/lib/loader/node-loaders"; +import fs from "fs"; + +// 鏈湴SVG鍥炬爣鐩綍 +const iconsDir = "./src/assets/icons"; + +// 璇诲彇鏈湴 SVG 鐩綍锛岃嚜鍔ㄧ敓鎴� safelist +const generateSafeList = () => { + try { + return fs + .readdirSync(iconsDir) + .filter((file) => file.endsWith(".svg")) + .map((file) => `i-svg:${file.replace(".svg", "")}`); + } catch (error) { + console.error("鏃犳硶璇诲彇鍥炬爣鐩綍:", error); + return []; + } +}; + +export default defineConfig({ + // 鑷畾涔夊揩鎹风被 + shortcuts: { + "wh-full": "w-full h-full", + "flex-center": "flex justify-center items-center", + "flex-x-center": "flex justify-center", + "flex-y-center": "flex items-center", + "flex-x-start": "flex items-center justify-start", + "flex-x-between": "flex items-center justify-between", + "flex-x-end": "flex items-center justify-end", + }, + theme: { + colors: { + primary: "var(--el-color-primary)", + primary_dark: "var(--el-color-primary-light-5)", + }, + }, + presets: [ + presetUno(), + presetAttributify(), + presetIcons({ + // 棰濆灞炴�� + extraProperties: { + display: "inline-block", + width: "1em", + height: "1em", + }, + // 鍥捐〃闆嗗悎 + collections: { + // svg 鏄浘鏍囬泦鍚堝悕绉帮紝浣跨敤 `i-svg:鍥炬爣鍚峘 璋冪敤 + svg: FileSystemIconLoader(iconsDir, (svg) => { + // 濡傛灉 `fill` 娌℃湁瀹氫箟锛屽垯娣诲姞 `fill="currentColor"` + return svg.includes('fill="') ? svg : svg.replace(/^<svg /, '<svg fill="currentColor" '); + }), + }, + }), + presetTypography(), + presetWebFonts({ + fonts: { + // ... + }, + }), + ], + safelist: generateSafeList(), + transformers: [transformerDirectives(), transformerVariantGroup()], +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f9e8b05 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,201 @@ +import vue from "@vitejs/plugin-vue"; +import { type UserConfig, type ConfigEnv, loadEnv, defineConfig } from "vite"; + +import AutoImport from "unplugin-auto-import/vite"; +import Components from "unplugin-vue-components/vite"; +import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; + +import mockDevServerPlugin from "vite-plugin-mock-dev-server"; + +import UnoCSS from "unocss/vite"; +import { resolve } from "path"; +import { name, version, engines, dependencies, devDependencies } from "./package.json"; + +// 骞冲彴鐨勫悕绉般�佺増鏈�佽繍琛屾墍闇�鐨� node 鐗堟湰銆佷緷璧栥�佹瀯寤烘椂闂寸殑绫诲瀷鎻愮ず +const __APP_INFO__ = { + pkg: { name, version, engines, dependencies, devDependencies }, + buildTimestamp: Date.now(), +}; + +const pathSrc = resolve(__dirname, "src"); +// Vite閰嶇疆 https://cn.vitejs.dev/config +export default defineConfig(({ mode }: ConfigEnv): UserConfig => { + const env = loadEnv(mode, process.cwd()); + return { + base: env.VITE_APP_BASE_PATH, + resolve: { + alias: { + "@": pathSrc, + }, + }, + css: { + preprocessorOptions: { + // 瀹氫箟鍏ㄥ眬 SCSS 鍙橀噺 + scss: { + api: "modern-compiler", + additionalData: ` + @use "@/styles/variables.scss" as *; + `, + }, + }, + }, + server: { + host: "0.0.0.0", + port: +env.VITE_APP_PORT, + open: false, + }, + plugins: [ + vue(), + env.VITE_MOCK_DEV_SERVER === "true" ? mockDevServerPlugin() : null, + UnoCSS(), + // 鑷姩瀵煎叆閰嶇疆 https://github.com/sxzz/element-plus-best-practices/blob/main/vite.config.ts + AutoImport({ + imports: ["vue", "@vueuse/core", "pinia", "vue-router"], + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: false, + filepath: "./.eslintrc-auto-import.json", + globalsPropValue: true, + }, + vueTemplate: true, + // 瀵煎叆鍑芥暟绫诲瀷澹版槑鏂囦欢璺緞 (false:鍏抽棴鑷姩鐢熸垚) + dts: false, + // dts: "src/types/auto-imports.d.ts", + }), + Components({ + resolvers: [ + // 瀵煎叆 Element Plus 缁勪欢 + ElementPlusResolver(), + ], + // 鎸囧畾鑷畾涔夌粍浠朵綅缃�(榛樿:src/components) + dirs: ["src/components", "src/**/components"], + // 瀵煎叆缁勪欢绫诲瀷澹版槑鏂囦欢璺緞 (false:鍏抽棴鑷姩鐢熸垚) + dts: false, + // dts: "src/types/components.d.ts", + }), + ], + // 棰勫姞杞介」鐩繀闇�鐨勭粍浠� + optimizeDeps: { + include: [ + "vue", + "vue-router", + "element-plus", + "pinia", + "axios", + "@vueuse/core", + "path-to-regexp", + "echarts", + "@wangeditor/editor", + "@wangeditor/editor-for-vue", + "path-browserify", + "element-plus/es/components/form/style/css", + "element-plus/es/components/form-item/style/css", + "element-plus/es/components/button/style/css", + "element-plus/es/components/input/style/css", + "element-plus/es/components/input-number/style/css", + "element-plus/es/components/switch/style/css", + "element-plus/es/components/upload/style/css", + "element-plus/es/components/menu/style/css", + "element-plus/es/components/col/style/css", + "element-plus/es/components/icon/style/css", + "element-plus/es/components/row/style/css", + "element-plus/es/components/tag/style/css", + "element-plus/es/components/dialog/style/css", + "element-plus/es/components/loading/style/css", + "element-plus/es/components/radio/style/css", + "element-plus/es/components/radio-group/style/css", + "element-plus/es/components/popover/style/css", + "element-plus/es/components/scrollbar/style/css", + "element-plus/es/components/tooltip/style/css", + "element-plus/es/components/dropdown/style/css", + "element-plus/es/components/dropdown-menu/style/css", + "element-plus/es/components/dropdown-item/style/css", + "element-plus/es/components/sub-menu/style/css", + "element-plus/es/components/menu-item/style/css", + "element-plus/es/components/divider/style/css", + "element-plus/es/components/card/style/css", + "element-plus/es/components/link/style/css", + "element-plus/es/components/breadcrumb/style/css", + "element-plus/es/components/breadcrumb-item/style/css", + "element-plus/es/components/table/style/css", + "element-plus/es/components/tree-select/style/css", + "element-plus/es/components/table-column/style/css", + "element-plus/es/components/select/style/css", + "element-plus/es/components/option/style/css", + "element-plus/es/components/pagination/style/css", + "element-plus/es/components/tree/style/css", + "element-plus/es/components/alert/style/css", + "element-plus/es/components/radio-button/style/css", + "element-plus/es/components/checkbox-group/style/css", + "element-plus/es/components/checkbox/style/css", + "element-plus/es/components/tabs/style/css", + "element-plus/es/components/tab-pane/style/css", + "element-plus/es/components/rate/style/css", + "element-plus/es/components/date-picker/style/css", + "element-plus/es/components/notification/style/css", + "element-plus/es/components/image/style/css", + "element-plus/es/components/statistic/style/css", + "element-plus/es/components/watermark/style/css", + "element-plus/es/components/config-provider/style/css", + "element-plus/es/components/text/style/css", + "element-plus/es/components/drawer/style/css", + "element-plus/es/components/color-picker/style/css", + "element-plus/es/components/backtop/style/css", + "element-plus/es/components/message-box/style/css", + "element-plus/es/components/skeleton/style/css", + "element-plus/es/components/skeleton/style/css", + "element-plus/es/components/skeleton-item/style/css", + "element-plus/es/components/badge/style/css", + "element-plus/es/components/steps/style/css", + "element-plus/es/components/step/style/css", + "element-plus/es/components/avatar/style/css", + "element-plus/es/components/descriptions/style/css", + "element-plus/es/components/descriptions-item/style/css", + "element-plus/es/components/checkbox-group/style/css", + "element-plus/es/components/progress/style/css", + "element-plus/es/components/image-viewer/style/css", + "element-plus/es/components/empty/style/css", + ], + }, + // 鏋勫缓閰嶇疆 + build: { + chunkSizeWarningLimit: 2000, // 娑堥櫎鎵撳寘澶у皬瓒呰繃500kb璀﹀憡 + minify: "terser", // Vite 2.6.x 浠ヤ笂闇�瑕侀厤缃� minify: "terser", terserOptions 鎵嶈兘鐢熸晥 + terserOptions: { + compress: { + keep_infinity: true, // 闃叉 Infinity 琚帇缂╂垚 1/0锛岃繖鍙兘浼氬鑷� Chrome 涓婄殑鎬ц兘闂 + drop_console: true, // 鐢熶骇鐜鍘婚櫎 console + drop_debugger: true, // 鐢熶骇鐜鍘婚櫎 debugger + }, + format: { + comments: false, // 鍒犻櫎娉ㄩ噴 + }, + }, + rollupOptions: { + output: { + // 鐢ㄤ簬浠庡叆鍙g偣鍒涘缓鐨勫潡鐨勬墦鍖呰緭鍑烘牸寮廩name]琛ㄧず鏂囦欢鍚�,[hash]琛ㄧず璇ユ枃浠跺唴瀹筯ash鍊� + entryFileNames: "js/[name].[hash].js", + // 鐢ㄤ簬鍛藉悕浠g爜鎷嗗垎鏃跺垱寤虹殑鍏变韩鍧楃殑杈撳嚭鍛藉悕 + chunkFileNames: "js/[name].[hash].js", + // 鐢ㄤ簬杈撳嚭闈欐�佽祫婧愮殑鍛藉悕锛孾ext]琛ㄧず鏂囦欢鎵╁睍鍚� + assetFileNames: (assetInfo: any) => { + const info = assetInfo.name.split("."); + let extType = info[info.length - 1]; + // console.log('鏂囦欢淇℃伅', assetInfo.name) + if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) { + extType = "media"; + } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) { + extType = "img"; + } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) { + extType = "fonts"; + } + return `${extType}/[name].[hash].[ext]`; + }, + }, + }, + }, + define: { + __APP_INFO__: JSON.stringify(__APP_INFO__), + }, + }; +}); -- Gitblit v1.9.1