Browse Source

feat(Breadcrumbe): Add Breadcrumb component

style: change function to arrow function
kailong321200875 3 years ago
parent
commit
4612e5544b
55 changed files with 586 additions and 270 deletions
  1. 12 0
      mock/user/index.ts
  2. 30 43
      pnpm-lock.yaml
  3. 1 1
      src/App.vue
  4. 4 0
      src/api/login/index.ts
  5. 89 0
      src/components/Breadcrumb/src/Breadcrumb.vue
  6. 31 0
      src/components/Breadcrumb/src/helper.ts
  7. 1 2
      src/components/Collapse/src/Collapse.vue
  8. 26 1
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  9. 6 6
      src/components/Form/src/Form.vue
  10. 2 2
      src/components/Form/src/components/useRenderChcekbox.tsx
  11. 2 2
      src/components/Form/src/components/useRenderRadio.tsx
  12. 3 3
      src/components/Form/src/components/useRenderSelect.tsx
  13. 7 7
      src/components/Form/src/helper.ts
  14. 2 7
      src/components/Icon/src/Icon.vue
  15. 4 11
      src/components/InputPassword/src/InputPassword.vue
  16. 2 8
      src/components/LocaleDropdown/src/LocaleDropdown.vue
  17. 4 9
      src/components/Menu/src/Menu.vue
  18. 3 3
      src/components/Menu/src/components/useRenderMenuItem.tsx
  19. 4 4
      src/components/Menu/src/components/useRenderMenuTitle.tsx
  20. 4 44
      src/components/Menu/src/helper.ts
  21. 1 1
      src/components/Screenfull/src/Screenfull.vue
  22. 1 1
      src/components/SizeDropdown/src/SizeDropdown.vue
  23. 1 1
      src/components/TagsView/src/TagsView.vue
  24. 2 7
      src/components/ThemeSwitch/src/ThemeSwitch.vue
  25. 9 5
      src/components/UserInfo/src/UserInfo.vue
  26. 1 1
      src/components/index.ts
  27. 3 1
      src/config/app.ts
  28. 2 2
      src/hooks/web/useAxios.ts
  29. 1 1
      src/hooks/web/useCache.ts
  30. 1 1
      src/hooks/web/useConfigGlobal.ts
  31. 2 2
      src/hooks/web/useDesign.ts
  32. 3 3
      src/hooks/web/useForm.ts
  33. 5 3
      src/hooks/web/useI18n.ts
  34. 1 1
      src/hooks/web/useIcon.ts
  35. 3 3
      src/hooks/web/useLocale.ts
  36. 6 5
      src/hooks/web/useNProgress.ts
  37. 1 1
      src/hooks/web/useTitle.ts
  38. 28 14
      src/layout/Layout.vue
  39. 1 1
      src/main.ts
  40. 1 1
      src/plugins/elementPlus/index.ts
  41. 1 1
      src/plugins/vueI18n/helper.ts
  42. 2 2
      src/plugins/vueI18n/index.ts
  43. 2 2
      src/router/index.ts
  44. 1 1
      src/store/index.ts
  45. 7 1
      src/store/modules/app.ts
  46. 1 1
      src/store/modules/locale.ts
  47. 1 1
      src/store/modules/permission.ts
  48. 1 1
      src/store/modules/tagsView.ts
  49. 11 11
      src/utils/color.ts
  50. 4 4
      src/utils/index.ts
  51. 21 21
      src/utils/is.ts
  52. 15 14
      src/utils/routerHelper.ts
  53. 207 0
      src/utils/tree.ts
  54. 1 1
      src/utils/tsxHelper.ts
  55. 1 1
      src/views/Login/components/LoginForm.vue

+ 12 - 0
mock/user/index.ts

@@ -50,5 +50,17 @@ export default [
         }
       }
     }
+  },
+  // 退出接口
+  {
+    url: '/user/loginOut',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        code: result_code,
+        data: null
+      }
+    }
   }
 ] as MockMethod[]

+ 30 - 43
pnpm-lock.yaml

@@ -33,6 +33,7 @@ specifiers:
   lodash-es: ^4.17.21
   mockjs: ^1.1.0
   nprogress: ^0.2.0
+  path-to-regexp: ^6.2.0
   pinia: ^2.0.9
   postcss: ^8.4.5
   postcss-html: ^1.3.0
@@ -41,7 +42,6 @@ specifiers:
   pretty-quick: ^3.1.3
   qs: ^6.10.3
   rimraf: ^3.0.2
-  screenfull: ^6.0.0
   stylelint: ^14.2.0
   stylelint-config-html: ^1.0.0
   stylelint-config-prettier: ^9.0.3
@@ -74,9 +74,9 @@ dependencies:
   lodash-es: registry.nlark.com/lodash-es/4.17.21
   mockjs: registry.npmmirror.com/mockjs/1.1.0
   nprogress: registry.npmmirror.com/nprogress/0.2.0
+  path-to-regexp: registry.npmmirror.com/path-to-regexp/6.2.0
   pinia: registry.npmmirror.com/pinia/2.0.9_typescript@4.5.4+vue@3.2.26
   qs: registry.npmmirror.com/qs/6.10.3
-  screenfull: registry.npmmirror.com/screenfull/6.0.0
   vue: registry.npmmirror.com/vue/3.2.26
   vue-i18n: registry.npmmirror.com/vue-i18n/9.1.9_vue@3.2.26
   vue-router: registry.npmmirror.com/vue-router/4.0.12_vue@3.2.26
@@ -1732,7 +1732,7 @@ packages:
       {
         integrity: sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc=,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/eslint-utils/download/eslint-utils-2.1.0.tgz
+        tarball: https://registry.nlark.com/eslint-utils/download/eslint-utils-2.1.0.tgz?cache=0&sync_timestamp=1631600361784&other_urls=https%3A%2F%2Fregistry.nlark.com%2Feslint-utils%2Fdownload%2Feslint-utils-2.1.0.tgz
       }
     name: eslint-utils
     version: 2.1.0
@@ -4246,17 +4246,6 @@ packages:
     version: 1.0.7
     dev: true
 
-  registry.nlark.com/path-to-regexp/6.2.0:
-    resolution:
-      {
-        integrity: sha1-97OAMzYQTDRoia3s5hRmkjBkXzg=,
-        registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/path-to-regexp/download/path-to-regexp-6.2.0.tgz
-      }
-    name: path-to-regexp
-    version: 6.2.0
-    dev: true
-
   registry.nlark.com/path-type/4.0.0:
     resolution:
       {
@@ -4959,7 +4948,7 @@ packages:
       {
         integrity: sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1631500167672&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz
+        tarball: https://registry.nlark.com/semver/download/semver-5.7.1.tgz
       }
     name: semver
     version: 5.7.1
@@ -4971,7 +4960,7 @@ packages:
       {
         integrity: sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1631500167672&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz
+        tarball: https://registry.nlark.com/semver/download/semver-6.3.0.tgz
       }
     name: semver
     version: 6.3.0
@@ -7066,8 +7055,8 @@ packages:
       vue-i18n:
         optional: true
     dependencies:
-      '@intlify/message-compiler': registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.27
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
+      '@intlify/message-compiler': registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.28
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.28
       jsonc-eslint-parser: registry.npmmirror.com/jsonc-eslint-parser/1.4.1
       source-map: registry.nlark.com/source-map/0.6.1
       vue-i18n: registry.npmmirror.com/vue-i18n/9.1.9_vue@3.2.26
@@ -7123,18 +7112,18 @@ packages:
       source-map: registry.nlark.com/source-map/0.6.1
     dev: false
 
-  registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.27:
+  registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.28:
     resolution:
       {
-        integrity: sha512-T3mBTm0559VX6l+lh8p5gDJ9/IS1XbVXeeMNJ2zTzxrf4lXg8OuotNjaxG3ZsuauQ5OqqlArkMYryXGyZnHolA==,
+        integrity: sha512-NBH9fZyitN2cijGt8bmU1W7ZPdhKbgW01L1RxJKFJW0cRaCmknJq63Aif1Q6xcxKt9ZhPbvIKHgPGzg1nWMfeA==,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/@intlify/message-compiler/download/@intlify/message-compiler-9.2.0-beta.27.tgz
+        tarball: https://registry.npmmirror.com/@intlify/message-compiler/download/@intlify/message-compiler-9.2.0-beta.28.tgz
       }
     name: '@intlify/message-compiler'
-    version: 9.2.0-beta.27
+    version: 9.2.0-beta.28
     engines: { node: '>= 12' }
     dependencies:
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.28
       source-map: registry.nlark.com/source-map/0.6.1
     dev: true
 
@@ -7178,15 +7167,15 @@ packages:
     engines: { node: '>= 10' }
     dev: false
 
-  registry.npmmirror.com/@intlify/shared/9.2.0-beta.27:
+  registry.npmmirror.com/@intlify/shared/9.2.0-beta.28:
     resolution:
       {
-        integrity: sha512-+Av77mIHy0qFkAq96mMAQGYcColMGN7e5+rUsyn3XxBw6oC3AGqYn/cQ6U/T3qOrzcHgcA+etAaLN3IxFqkJDw==,
+        integrity: sha512-JBMcoj1D4kSAma7Vb0+d8z6lPLIn7hIdZJPxbU8bgeMMniwKLoIS/jGlEfrZihsB5+otckPeQp203z8skwVS0w==,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/@intlify/shared/download/@intlify/shared-9.2.0-beta.27.tgz
+        tarball: https://registry.npmmirror.com/@intlify/shared/download/@intlify/shared-9.2.0-beta.28.tgz
       }
     name: '@intlify/shared'
-    version: 9.2.0-beta.27
+    version: 9.2.0-beta.28
     engines: { node: '>= 12' }
     dev: true
 
@@ -7212,7 +7201,7 @@ packages:
         optional: true
     dependencies:
       '@intlify/bundle-utils': registry.npmmirror.com/@intlify/bundle-utils/2.2.0_vue-i18n@9.1.9
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.28
       '@rollup/pluginutils': registry.npmmirror.com/@rollup/pluginutils/4.1.2
       debug: registry.npmmirror.com/debug/4.3.3
       fast-glob: registry.nlark.com/fast-glob/3.2.7
@@ -9689,7 +9678,7 @@ packages:
       {
         integrity: sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz
+        tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz?cache=0&sync_timestamp=1636378650851&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-1.3.0.tgz
       }
     name: eslint-visitor-keys
     version: 1.3.0
@@ -9701,7 +9690,7 @@ packages:
       {
         integrity: sha1-9lMoJZMFknOSyTjtROsKXJsr0wM=,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-2.1.0.tgz
+        tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-2.1.0.tgz?cache=0&sync_timestamp=1636378650851&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-2.1.0.tgz
       }
     name: eslint-visitor-keys
     version: 2.1.0
@@ -11134,6 +11123,16 @@ packages:
     engines: { node: '>=8' }
     dev: true
 
+  registry.npmmirror.com/path-to-regexp/6.2.0:
+    resolution:
+      {
+        integrity: sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/path-to-regexp/download/path-to-regexp-6.2.0.tgz
+      }
+    name: path-to-regexp
+    version: 6.2.0
+
   registry.npmmirror.com/picocolors/1.0.0:
     resolution:
       {
@@ -11609,18 +11608,6 @@ packages:
       tslib: registry.npmmirror.com/tslib/2.3.1
     dev: true
 
-  registry.npmmirror.com/screenfull/6.0.0:
-    resolution:
-      {
-        integrity: sha512-LGY0nhNQkC4FX4DT4pZdJ5cZH5EOz9Gfh9KcVMl779pS677k4IV1Wv7sY/CwC9VKFT21fYgCh7zkTVVefi5XKA==,
-        registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/screenfull/download/screenfull-6.0.0.tgz
-      }
-    name: screenfull
-    version: 6.0.0
-    engines: { node: ^14.13.1 || >=16.0.0 }
-    dev: false
-
   registry.npmmirror.com/shebang-regex/3.0.0:
     resolution:
       {
@@ -12307,7 +12294,7 @@ packages:
       esbuild: registry.npmmirror.com/esbuild/0.11.3
       fast-glob: registry.nlark.com/fast-glob/3.2.7
       mockjs: registry.npmmirror.com/mockjs/1.1.0
-      path-to-regexp: registry.nlark.com/path-to-regexp/6.2.0
+      path-to-regexp: registry.npmmirror.com/path-to-regexp/6.2.0
       vite: registry.npmmirror.com/vite/2.7.10_less@4.1.2
     transitivePeerDependencies:
       - rollup

+ 1 - 1
src/App.vue

@@ -8,7 +8,7 @@ const appStore = useAppStore()
 
 const size = computed(() => appStore.size)
 
-function initDark() {
+const initDark = () => {
   const isDarkTheme = isDark()
   appStore.setIsDark(isDarkTheme)
 }

+ 4 - 0
src/api/login/index.ts

@@ -9,3 +9,7 @@ export const loginApi = (data: UserLoginType) => {
     UserLoginType
   >)
 }
+
+export const loginOutApi = () => {
+  return request({ url: '/user/loginOut', method: 'get' })
+}

+ 89 - 0
src/components/Breadcrumb/src/Breadcrumb.vue

@@ -0,0 +1,89 @@
+<script lang="tsx">
+import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
+import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
+import { useRouter } from 'vue-router'
+// import { compile } from 'path-to-regexp'
+import { usePermissionStore } from '@/store/modules/permission'
+import { filterBreadcrumb } from './helper'
+import { filter, treeToList } from '@/utils/tree'
+import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Icon } from '@/components/Icon'
+
+export default defineComponent({
+  name: 'Breadcrumb',
+  setup() {
+    const { currentRoute } = useRouter()
+
+    const { t } = useI18n()
+
+    const levelList = ref<AppRouteRecordRaw[]>([])
+
+    const permissionStore = usePermissionStore()
+
+    const menuRouters = computed(() => {
+      const routers = permissionStore.getRouters
+      return filterBreadcrumb(routers)
+    })
+
+    const getBreadcrumb = () => {
+      const currentPath = currentRoute.value.path
+
+      levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
+        return node.path === currentPath
+      })
+    }
+
+    const renderBreadcrumb = () => {
+      const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
+      return breadcrumbList.map((v) => {
+        const disabled = v.redirect === 'noredirect'
+        const meta = v.meta as RouteMeta
+        return (
+          <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
+            {meta?.icon ? (
+              <>
+                <Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)}
+              </>
+            ) : (
+              t(v?.meta?.title)
+            )}
+          </ElBreadcrumbItem>
+        )
+      })
+    }
+
+    watch(
+      () => currentRoute.value,
+      (route: RouteLocationNormalizedLoaded) => {
+        if (route.path.startsWith('/redirect/')) {
+          return
+        }
+        getBreadcrumb()
+      },
+      {
+        immediate: true
+      }
+    )
+
+    return () => (
+      <ElBreadcrumb separator="/" class="flex items-center h-full ml-[10px]">
+        <TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
+          {renderBreadcrumb()}
+        </TransitionGroup>
+      </ElBreadcrumb>
+    )
+  }
+})
+</script>
+
+<style lang="less" scoped>
+:deep(.el-breadcrumb__item) {
+  display: flex;
+
+  .el-breadcrumb__inner {
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 31 - 0
src/components/Breadcrumb/src/helper.ts

@@ -0,0 +1,31 @@
+import { pathResolve } from '@/utils/routerHelper'
+import type { RouteMeta } from 'vue-router'
+
+export const filterBreadcrumb = (
+  routes: AppRouteRecordRaw[],
+  parentPath = ''
+): AppRouteRecordRaw[] => {
+  const res: AppRouteRecordRaw[] = []
+
+  for (const route of routes) {
+    const meta = route?.meta as RouteMeta
+    if (meta.hidden && !meta.showMainRoute) {
+      continue
+    }
+
+    const data: AppRouteRecordRaw =
+      !meta.alwaysShow && route.children?.length === 1
+        ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
+        : { ...route }
+
+    data.path = pathResolve(parentPath, data.path)
+
+    if (data.children) {
+      data.children = filterBreadcrumb(data.children, data.path)
+    }
+    if (data) {
+      res.push(data)
+    }
+  }
+  return res
+}

+ 1 - 2
src/components/Collapse/src/Collapse.vue

@@ -1,13 +1,12 @@
 <script setup lang="ts">
 import { computed, unref } from 'vue'
-import { Icon } from '@/components/Icon'
 import { useAppStore } from '@/store/modules/app'
 
 const appStore = useAppStore()
 
 const collapse = computed(() => appStore.getCollapse)
 
-function toggleCollapse() {
+const toggleCollapse = () => {
   const collapsed = unref(collapse)
   appStore.setCollapse(!collapsed)
 }

+ 26 - 1
src/components/ConfigGlobal/src/ConfigGlobal.vue

@@ -1,8 +1,33 @@
 <script setup lang="ts">
-import { provide, computed } from 'vue'
+import { provide, computed, watch } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { ElConfigProvider } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
+import { useWindowSize } from '@vueuse/core'
+import { useAppStore } from '@/store/modules/app'
+import { setCssVar } from '@/utils'
+
+const appStore = useAppStore()
+
+const { width } = useWindowSize()
+
+watch(
+  () => width.value,
+  (width: number) => {
+    if (width < 768) {
+      !appStore.getMobile ? appStore.setMobile(true) : undefined
+      setCssVar('--left-menu-min-width', '0')
+      appStore.setCollapse(true)
+      appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
+    } else {
+      appStore.getMobile ? appStore.setMobile(false) : undefined
+      setCssVar('--left-menu-min-width', '64px')
+    }
+  },
+  {
+    immediate: true
+  }
+)
 
 const localeStore = useLocaleStore()
 

+ 6 - 6
src/components/Form/src/Form.vue

@@ -62,7 +62,7 @@ export default defineComponent({
     })
 
     // 对表单赋值
-    function setValues(data: FormSetValuesType[]) {
+    const setValues = (data: FormSetValuesType[]) => {
       if (!data.length) return
       const formData: Recordable = {}
       for (const v of data) {
@@ -89,7 +89,7 @@ export default defineComponent({
     )
 
     // 渲染包裹标签,是否使用栅格布局
-    function renderWrap() {
+    const renderWrap = () => {
       const content = isCol ? (
         <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
       ) : (
@@ -99,7 +99,7 @@ export default defineComponent({
     }
 
     // 是否要渲染el-col
-    function renderFormItemWrap() {
+    const renderFormItemWrap = () => {
       // hidden属性表示隐藏,不做渲染
       return schema
         .filter((v) => !v.hidden)
@@ -119,7 +119,7 @@ export default defineComponent({
     }
 
     // 渲染formItem
-    function renderFormItem(item: FormSchema) {
+    const renderFormItem = (item: FormSchema) => {
       // 单独给只有options属性的组件做判断
       const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
       const slotsMap: Recordable = {
@@ -162,7 +162,7 @@ export default defineComponent({
     }
 
     // 渲染options
-    function renderOptions(item: FormSchema) {
+    const renderOptions = (item: FormSchema) => {
       switch (item.component) {
         case 'Select':
           const { renderSelectOptions } = useRenderSelect(slots)
@@ -181,7 +181,7 @@ export default defineComponent({
     }
 
     // 过滤传入Form组件的属性
-    function getFormBindValue() {
+    const getFormBindValue = () => {
       // 避免在标签上出现多余的属性
       const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
       const props = { ...unref(getProps) }

+ 2 - 2
src/components/Form/src/components/useRenderChcekbox.tsx

@@ -1,8 +1,8 @@
 import { ElCheckbox, ElCheckboxButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
-export function useRenderChcekbox() {
-  function renderChcekboxOptions(item: FormSchema) {
+export const useRenderChcekbox = () => {
+  const renderChcekboxOptions = (item: FormSchema) => {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 2 - 2
src/components/Form/src/components/useRenderRadio.tsx

@@ -1,8 +1,8 @@
 import { ElRadio, ElRadioButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
-export function useRenderRadio() {
-  function renderRadioOptions(item: FormSchema) {
+export const useRenderRadio = () => {
+  const renderRadioOptions = (item: FormSchema) => {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 3 - 3
src/components/Form/src/components/useRenderSelect.tsx

@@ -2,9 +2,9 @@ import { ElOption, ElOptionGroup } from 'element-plus'
 import { getSlot } from '@/utils/tsxHelper'
 import { Slots } from 'vue'
 
-export function useRenderSelect(slots: Slots) {
+export const useRenderSelect = (slots: Slots) => {
   // 渲染 select options
-  function renderSelectOptions(item: FormSchema) {
+  const renderSelectOptions = (item: FormSchema) => {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     return item?.componentProps?.options?.map((option) => {
@@ -25,7 +25,7 @@ export function useRenderSelect(slots: Slots) {
   }
 
   // 渲染 select option item
-  function renderSelectOptionItem(item: FormSchema, option: ComponentOptions) {
+  const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 7 - 7
src/components/Form/src/helper.ts

@@ -17,7 +17,7 @@ interface PlaceholderMoel {
  * @returns 返回提示信息对象
  * @description 用于自动设置placeholder
  */
-export function setTextPlaceholder(schema: FormSchema): PlaceholderMoel {
+export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
   const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
   const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
   if (textMap.includes(schema?.component as string)) {
@@ -53,7 +53,7 @@ export function setTextPlaceholder(schema: FormSchema): PlaceholderMoel {
  * @returns 返回栅格属性
  * @description 合并传入进来的栅格属性
  */
-export function setGridProp(col: ColProps = {}): ColProps {
+export const setGridProp = (col: ColProps = {}): ColProps => {
   const colProps: ColProps = {
     // 如果有span,代表用户优先级更高,所以不需要默认栅格
     ...(col.span
@@ -75,7 +75,7 @@ export function setGridProp(col: ColProps = {}): ColProps {
  * @param item 传入的组件属性
  * @returns 默认添加 clearable 属性
  */
-export function setComponentProps(item: FormSchema): Recordable {
+export const setComponentProps = (item: FormSchema): Recordable => {
   const notNeedClearable = ['ColorPicker']
   const componentProps: Recordable = notNeedClearable.includes(item.component as string)
     ? { ...item.componentProps }
@@ -94,11 +94,11 @@ export function setComponentProps(item: FormSchema): Recordable {
  * @param slotsProps 插槽属性
  * @param field 字段名
  */
-export function setItemComponentSlots(
+export const setItemComponentSlots = (
   slots: Slots,
   slotsProps: Recordable = {},
   field: string
-): Recordable {
+): Recordable => {
   const slotObj: Recordable = {}
   for (const key in slotsProps) {
     if (slotsProps[key]) {
@@ -118,7 +118,7 @@ export function setItemComponentSlots(
  * @returns FormMoel
  * @description 生成对应的formModel
  */
-export function initModel(schema: FormSchema[], formModel: Recordable) {
+export const initModel = (schema: FormSchema[], formModel: Recordable) => {
   const model: Recordable = { ...formModel }
   schema.map((v) => {
     // 如果是hidden,就删除对应的值
@@ -138,7 +138,7 @@ export function initModel(schema: FormSchema[], formModel: Recordable) {
  * @param field 字段名
  * @returns 返回FormIiem插槽
  */
-export function setFormItemSlots(slots: Slots, field: string): Recordable {
+export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
   const slotObj: Recordable = {}
   if (slots[`${field}-error`]) {
     slotObj['error'] = (data: Recordable) => {

+ 2 - 7
src/components/Icon/src/Icon.vue

@@ -1,14 +1,9 @@
 <script setup lang="ts">
 import { computed, unref, ref, watch, nextTick } from 'vue'
 import { ElIcon } from 'element-plus'
-import { useDesign } from '@/hooks/web/useDesign'
 import { propTypes } from '@/utils/propTypes'
 import Iconify from '@purge-icons/generated'
 
-const { getPrefixCls } = useDesign()
-
-const prefixCls = getPrefixCls('icon')
-
 const props = defineProps({
   // icon name
   icon: propTypes.string,
@@ -34,7 +29,7 @@ const getIconifyStyle = computed(() => {
   }
 })
 
-async function updateIcon(icon: string) {
+const updateIcon = async (icon: string) => {
   if (unref(isLocal)) return
 
   const el = unref(elRef)
@@ -66,7 +61,7 @@ watch(
 </script>
 
 <template>
-  <ElIcon :class="prefixCls" :size="size" :color="color">
+  <ElIcon class="v-icon" :size="size" :color="color">
     <svg v-if="isLocal" aria-hidden="true">
       <use :xlink:href="symbolId" />
     </svg>

+ 4 - 11
src/components/InputPassword/src/InputPassword.vue

@@ -2,7 +2,6 @@
 import { ref, unref, computed, watch } from 'vue'
 import { ElInput } from 'element-plus'
 import { propTypes } from '@/utils/propTypes'
-import { useDesign } from '@/hooks/web/useDesign'
 import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
 import { zxcvbn } from '@zxcvbn-ts/core'
 import type { ZxcvbnResult } from '@zxcvbn-ts/core'
@@ -25,15 +24,10 @@ const { configGlobal } = useConfigGlobal()
 
 const emit = defineEmits(['update:modelValue'])
 
-// 生成class前缀
-const { getPrefixCls } = useDesign()
-
-const prefixCls = ref(getPrefixCls('input-password'))
-
 // 设置input的type属性
 const textType = ref<'password' | 'text'>('password')
 
-function changeTextType() {
+const changeTextType = () => {
   textType.value = unref(textType) === 'text' ? 'password' : 'text'
 }
 
@@ -61,7 +55,7 @@ const getIconName = computed(() =>
 </script>
 
 <template>
-  <div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
+  <div :class="['v-input-password', `v-input-password--${configGlobal?.size}`]">
     <ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
       <template #suffix>
         <Icon class="el-input__icon cursor-pointer" :icon="getIconName" @click="changeTextType" />
@@ -69,10 +63,9 @@ const getIconName = computed(() =>
     </ElInput>
     <div
       v-if="strength"
-      :class="`${prefixCls}__bar`"
-      class="relative h-6px mt-10px mb-6px mr-auto ml-auto"
+      class="v-input-password__bar relative h-6px mt-10px mb-6px mr-auto ml-auto"
     >
-      <div :class="`${prefixCls}__bar--fill`" :data-score="getPasswordStrength"></div>
+      <div class="v-input-password__bar--fill" :data-score="getPasswordStrength"></div>
     </div>
   </div>
 </template>

+ 2 - 8
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -10,7 +10,7 @@ const langMap = computed(() => localeStore.getLocaleMap)
 
 const currentLang = computed(() => localeStore.getLocale)
 
-function setLang(lang: LocaleType) {
+const setLang = (lang: LocaleType) => {
   if (lang === unref(currentLang).lang) return
   // 需要重新加载页面让整个语言多初始化
   window.location.reload()
@@ -24,13 +24,7 @@ function setLang(lang: LocaleType) {
 
 <template>
   <ElDropdown trigger="click" @command="setLang">
-    <Icon
-      :size="18"
-      icon="ion:language-sharp"
-      color="var(--el-text-color-primary)"
-      class="cursor-pointer"
-      :class="$attrs.class"
-    />
+    <Icon :size="18" icon="ion:language-sharp" class="cursor-pointer" :class="$attrs.class" />
     <template #dropdown>
       <ElDropdownMenu>
         <ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">

+ 4 - 9
src/components/Menu/src/Menu.vue

@@ -1,7 +1,6 @@
 <script lang="tsx">
 import { computed, defineComponent } from 'vue'
 import { ElMenu, ElScrollbar } from 'element-plus'
-import { useDesign } from '@/hooks/web/useDesign'
 import { useAppStore } from '@/store/modules/app'
 import { usePermissionStore } from '@/store/modules/permission'
 import type { LayoutType } from '@/config/app'
@@ -18,10 +17,6 @@ export default defineComponent({
 
     const permissionStore = usePermissionStore()
 
-    const { getPrefixCls } = useDesign()
-
-    const preFixCls = getPrefixCls('menu')
-
     const menuMode = computed((): 'vertical' | 'horizontal' => {
       // 竖
       const vertical: LayoutType[] = ['classic']
@@ -46,7 +41,7 @@ export default defineComponent({
       return path
     })
 
-    function menuSelect(index: string) {
+    const menuSelect = (index: string) => {
       if (isUrl(index)) {
         window.open(index)
       } else {
@@ -57,8 +52,8 @@ export default defineComponent({
     return () => (
       <div
         class={[
-          preFixCls,
-          'h-[100%] overflow-hidden',
+          'v-menu',
+          'h-[100%] overflow-hidden z-100',
           appStore.getCollapse
             ? 'w-[var(--left-menu-min-width)]'
             : 'w-[var(--left-menu-max-width)]',
@@ -147,7 +142,7 @@ export default defineComponent({
 
   // 折叠动画的时候,就需要把文字给隐藏掉
   :deep(.horizontal-collapse-transition) {
-    transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
+    // transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
     .@{prefix-cls}__title {
       display: none;
     }

+ 3 - 3
src/components/Menu/src/components/useRenderMenuItem.tsx

@@ -6,11 +6,11 @@ import { useRenderMenuTitle } from './useRenderMenuTitle'
 import { useDesign } from '@/hooks/web/useDesign'
 import { pathResolve } from '@/utils/routerHelper'
 
-export function useRenderMenuItem(
+export const useRenderMenuItem = (
   allRouters: AppRouteRecordRaw[] = [],
   menuMode: 'vertical' | 'horizontal'
-) {
-  function renderMenuItem(routers?: AppRouteRecordRaw[]) {
+) => {
+  const renderMenuItem = (routers?: AppRouteRecordRaw[]) => {
     return (routers || allRouters).map((v) => {
       const meta = (v.meta ?? {}) as RouteMeta
       if (!meta.hidden) {

+ 4 - 4
src/components/Menu/src/components/useRenderMenuTitle.tsx

@@ -2,18 +2,18 @@ import type { RouteMeta } from 'vue-router'
 import { Icon } from '@/components/Icon'
 import { useI18n } from '@/hooks/web/useI18n'
 
-export function useRenderMenuTitle() {
-  function renderMenuTitle(meta: RouteMeta) {
+export const useRenderMenuTitle = () => {
+  const renderMenuTitle = (meta: RouteMeta) => {
     const { t } = useI18n()
     const { title = 'Please set title', icon } = meta
 
     return icon ? (
       <>
         <Icon icon={meta.icon}></Icon>
-        <span>{t(title as string)}</span>
+        <span class="v-menu__title">{t(title as string)}</span>
       </>
     ) : (
-      <span>{t(title as string)}</span>
+      <span class="v-menu__title">{t(title as string)}</span>
     )
   }
 

+ 4 - 44
src/components/Menu/src/helper.ts

@@ -1,11 +1,6 @@
 import type { RouteMeta } from 'vue-router'
 import { ref, unref } from 'vue'
-
-interface TreeConfig {
-  id: string
-  children: string
-  pid: string
-}
+import { findPath } from '@/utils/tree'
 
 type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
 
@@ -14,50 +9,15 @@ interface HasOneShowingChild {
   onlyOneChild?: OnlyOneChildType
 }
 
-const DEFAULT_CONFIG: TreeConfig = {
-  id: 'id',
-  children: 'children',
-  pid: 'pid'
-}
-
-const getConfig = (config: Partial<TreeConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
-
-export function getAllParentPath<T = Recordable>(treeData: T[], path: string) {
+export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
   const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
   return (menuList || []).map((item) => item.path)
 }
 
-export function findPath<T = any>(
-  tree: any,
-  func: Fn,
-  config: Partial<TreeConfig> = {}
-): T | T[] | null {
-  config = getConfig(config)
-  const path: T[] = []
-  const list = [...tree]
-  const visitedSet = new Set()
-  const { children } = config
-  while (list.length) {
-    const node = list[0]
-    if (visitedSet.has(node)) {
-      path.pop()
-      list.shift()
-    } else {
-      visitedSet.add(node)
-      node[children!] && list.unshift(...node[children!])
-      path.push(node)
-      if (func(node)) {
-        return path
-      }
-    }
-  }
-  return null
-}
-
-export function hasOneShowingChild(
+export const hasOneShowingChild = (
   children: AppRouteRecordRaw[] = [],
   parent: AppRouteRecordRaw
-): HasOneShowingChild {
+): HasOneShowingChild => {
   const onlyOneChild = ref<OnlyOneChildType>()
 
   const showingChildren = children.filter((v) => {

+ 1 - 1
src/components/Screenfull/src/Screenfull.vue

@@ -4,7 +4,7 @@ import { useFullscreen } from '@vueuse/core'
 
 const { toggle, isFullscreen } = useFullscreen()
 
-function toggleFullscreen() {
+const toggleFullscreen = () => {
   toggle()
 }
 </script>

+ 1 - 1
src/components/SizeDropdown/src/SizeDropdown.vue

@@ -9,7 +9,7 @@ const appStore = useAppStore()
 
 const sizeMap = computed(() => appStore.sizeMap)
 
-function setSize(size: ElememtPlusSzie) {
+const setSize = (size: ElememtPlusSzie) => {
   appStore.setSize(size)
 }
 </script>

+ 1 - 1
src/components/TagsView/src/TagsView.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div>tagsView</div>
+  <div class="h-[var(--tags-view-height)]">tagsView</div>
 </template>

+ 2 - 7
src/components/ThemeSwitch/src/ThemeSwitch.vue

@@ -2,7 +2,6 @@
 import { ref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
 import { ElSwitch } from 'element-plus'
-import { useDesign } from '@/hooks/web/useDesign'
 import { useIcon } from '@/hooks/web/useIcon'
 
 const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
@@ -14,21 +13,17 @@ const appStore = useAppStore()
 // 初始化获取是否是暗黑主题
 const isDark = ref(appStore.getIsDark)
 
-const { getPrefixCls } = useDesign()
-
-const prefixCls = getPrefixCls('theme-switch')
-
 // 设置switch的背景颜色
 const blackColor = 'var(--el-color-black)'
 
-function themeChange(val: boolean) {
+const themeChange = (val: boolean) => {
   appStore.setIsDark(val)
 }
 </script>
 
 <template>
   <ElSwitch
-    :class="prefixCls"
+    class="v-theme-switch"
     v-model="isDark"
     inline-prompt
     :border-color="blackColor"

+ 9 - 5
src/components/UserInfo/src/UserInfo.vue

@@ -4,6 +4,7 @@ import { useI18n } from '@/hooks/web/useI18n'
 import { useCache } from '@/hooks/web/useCache'
 import { resetRouter } from '@/router'
 import { useRouter } from 'vue-router'
+import { loginOutApi } from '@/api/login'
 
 const { t } = useI18n()
 
@@ -11,16 +12,19 @@ const { wsCache } = useCache()
 
 const { replace } = useRouter()
 
-function loginOut() {
+const loginOut = () => {
   ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
     confirmButtonText: t('common.ok'),
     cancelButtonText: t('common.cancel'),
     type: 'warning'
   })
-    .then(() => {
-      wsCache.clear()
-      resetRouter() // 重置静态路由表
-      replace('/login')
+    .then(async () => {
+      const res = await loginOutApi().catch(() => {})
+      if (res) {
+        wsCache.clear()
+        resetRouter() // 重置静态路由表
+        replace('/login')
+      }
     })
     .catch(() => {})
 }

+ 1 - 1
src/components/index.ts

@@ -1,6 +1,6 @@
 import type { App } from 'vue'
 import { Icon } from './Icon'
 
-export function setupGlobCom(app: App<Element>): void {
+export const setupGlobCom = (app: App<Element>): void => {
   app.component('Icon', Icon)
 }

+ 3 - 1
src/config/app.ts

@@ -24,6 +24,7 @@ export interface AppState {
   isDark: boolean
   size: ElememtPlusSzie
   sizeMap: ElememtPlusSzie[]
+  mobile: boolean
 }
 
 export const appModules: AppState = {
@@ -45,5 +46,6 @@ export const appModules: AppState = {
   showMenuTab: false, // 是否固定一级菜单
   isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
   size: wsCache.get('default') || 'default', // 组件尺寸
-  sizeMap: ['default', 'large', 'small']
+  sizeMap: ['default', 'large', 'small'],
+  mobile: false // 是否是移动端
 }

+ 2 - 2
src/hooks/web/useAxios.ts

@@ -6,8 +6,8 @@ import { config } from '@/config/axios/config'
 
 const { default_headers } = config
 
-export function useAxios() {
-  function request(option: AxiosConfig): AxiosPromise {
+export const useAxios = () => {
+  const request = (option: AxiosConfig): AxiosPromise => {
     const { url, method, params, data, headersType, responseType } = option
     return service({
       url: url,

+ 1 - 1
src/hooks/web/useCache.ts

@@ -6,7 +6,7 @@ import WebStorageCache from 'web-storage-cache'
 
 type CacheType = 'sessionStorage' | 'localStorage'
 
-export function useCache(type: CacheType = 'sessionStorage') {
+export const useCache = (type: CacheType = 'sessionStorage') => {
   const wsCache: WebStorageCache = new WebStorageCache({
     storage: type
   })

+ 1 - 1
src/hooks/web/useConfigGlobal.ts

@@ -1,6 +1,6 @@
 import { inject } from 'vue'
 
-export function useConfigGlobal() {
+export const useConfigGlobal = () => {
   const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
 
   return {

+ 2 - 2
src/hooks/web/useDesign.ts

@@ -1,13 +1,13 @@
 import variables from '@/styles/variables.module.less'
 
-export function useDesign() {
+export const useDesign = () => {
   const lessVariables = variables
 
   /**
    * @param scope 类名
    * @returns 返回空间名-类名
    */
-  function getPrefixCls(scope: string) {
+  const getPrefixCls = (scope: string) => {
     return `${lessVariables.namespace}-${scope}`
   }
 

+ 3 - 3
src/hooks/web/useForm.ts

@@ -2,7 +2,7 @@ import type { Form, FormExpose } from '@/components/Form'
 import type { ElForm } from 'element-plus'
 import { ref, unref, nextTick } from 'vue'
 
-export function useForm() {
+export const useForm = () => {
   // From实例
   const formRef = ref<typeof Form & FormExpose>()
 
@@ -13,12 +13,12 @@ export function useForm() {
    * @param ref Form实例
    * @param elRef ElForm实例
    */
-  function register(ref: typeof Form & FormExpose, elRef: ComponentRef<typeof ElForm>) {
+  const register = (ref: typeof Form & FormExpose, elRef: ComponentRef<typeof ElForm>) => {
     formRef.value = ref
     elFormRef.value = elRef
   }
 
-  async function getForm() {
+  const getForm = async () => {
     const form = unref(formRef)
     if (!form) {
       console.error('The form is not registered. Please use the register method to register')

+ 5 - 3
src/hooks/web/useI18n.ts

@@ -11,7 +11,7 @@ type I18nGlobalTranslation = {
 
 type I18nTranslationRestParameters = [string, any]
 
-function getKey(namespace: string | undefined, key: string) {
+const getKey = (namespace: string | undefined, key: string) => {
   if (!namespace) {
     return key
   }
@@ -21,9 +21,11 @@ function getKey(namespace: string | undefined, key: string) {
   return `${namespace}.${key}`
 }
 
-export function useI18n(namespace?: string): {
+export const useI18n = (
+  namespace?: string
+): {
   t: I18nGlobalTranslation
-} {
+} => {
   const normalFn = {
     t: (key: string) => {
       return getKey(namespace, key)

+ 1 - 1
src/hooks/web/useIcon.ts

@@ -2,6 +2,6 @@ import { h } from 'vue'
 import type { VNode } from 'vue'
 import { Icon } from '@/components/Icon'
 
-export function useIcon(props: IconTypes): VNode {
+export const useIcon = (props: IconTypes): VNode => {
   return h(Icon, props)
 }

+ 3 - 3
src/hooks/web/useLocale.ts

@@ -2,7 +2,7 @@ import { i18n } from '@/plugins/vueI18n'
 import { useLocaleStoreWithOut } from '@/store/modules/locale'
 import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
 
-function setI18nLanguage(locale: LocaleType) {
+const setI18nLanguage = (locale: LocaleType) => {
   const localeStore = useLocaleStoreWithOut()
 
   if (i18n.mode === 'legacy') {
@@ -16,10 +16,10 @@ function setI18nLanguage(locale: LocaleType) {
   setHtmlPageLang(locale)
 }
 
-export function useLocale() {
+export const useLocale = () => {
   // Switching the language will change the locale of useI18n
   // And submit to configuration modification
-  async function changeLocale(locale: LocaleType) {
+  const changeLocale = async (locale: LocaleType) => {
     const globalI18n = i18n.global
 
     const langModule = await import(`../../locales/${locale}.ts`)

+ 6 - 5
src/hooks/web/useNProgress.ts

@@ -6,11 +6,10 @@ import { useCssVar } from '@vueuse/core'
 
 const primaryColor = useCssVar('--el-color-primary', document.documentElement)
 
-export function useNProgress() {
+export const useNProgress = () => {
   NProgress.configure({ showSpinner: false } as NProgressOptions)
-  initColor()
 
-  async function initColor() {
+  const initColor = async () => {
     await nextTick()
     const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
     if (bar) {
@@ -18,11 +17,13 @@ export function useNProgress() {
     }
   }
 
-  function start() {
+  initColor()
+
+  const start = () => {
     NProgress.start()
   }
 
-  function done() {
+  const done = () => {
     NProgress.done()
   }
 

+ 1 - 1
src/hooks/web/useTitle.ts

@@ -5,7 +5,7 @@ import { useI18n } from '@/hooks/web/useI18n'
 
 const appStore = useAppStoreWithOut()
 
-export function useTitle(newTitle?: string) {
+export const useTitle = (newTitle?: string) => {
   const { t } = useI18n()
   const title = ref(
     newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle

+ 28 - 14
src/layout/Layout.vue

@@ -3,13 +3,13 @@ import { computed, defineComponent, KeepAlive } from 'vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useAppStore } from '@/store/modules/app'
 import { Menu } from '@/components/Menu'
-import { useDesign } from '@/hooks/web/useDesign'
 import { Collapse } from '@/components/Collapse'
 import { LocaleDropdown } from '@/components/LocaleDropdown'
 import { SizeDropdown } from '@/components/SizeDropdown'
 import { UserInfo } from '@/components/UserInfo'
 import { Screenfull } from '@/components/Screenfull'
-// import { TagsView } from '@/components/TagsView'
+import { Breadcrumb } from '@/components/Breadcrumb'
+import { TagsView } from '@/components/TagsView'
 
 const tagsViewStore = useTagsViewStore()
 
@@ -19,40 +19,50 @@ const getCaches = computed((): string[] => {
 
 const appStore = useAppStore()
 
-const classSuffix = computed(() => appStore.getLayout)
+const mobile = computed(() => appStore.getMobile)
+
+const collapse = computed(() => appStore.getCollapse)
 
-const { getPrefixCls } = useDesign()
+const classSuffix = computed(() => appStore.getLayout)
 
-const perFixCls = getPrefixCls('app')
+const handleClickOutside = () => {
+  appStore.setCollapse(true)
+}
 
 export default defineComponent({
   name: 'Layout',
   setup() {
     return () => (
-      <section
-        class={[perFixCls, `${perFixCls}__${classSuffix.value}`, 'w-[100%] h-[100%] relative']}
-      >
+      <section class={['v-app', `v-app__${classSuffix.value}`, 'w-[100%] h-[100%] relative']}>
+        {mobile.value && !collapse.value ? (
+          <div
+            class="absolute top-0 left-0 w-full h-full opacity-30 z-99 bg-[var(--el-color-black)]"
+            onClick={handleClickOutside}
+          ></div>
+        ) : undefined}
         <Menu class="absolute top-0 left-0"></Menu>
         <div
           class={[
-            `${perFixCls}-right`,
+            'v-app-right',
             'absolute top-0 h-[100%]',
-            appStore.getCollapse
+            collapse.value
               ? 'w-[calc(100%-var(--left-menu-min-width))]'
               : 'w-[calc(100%-var(--left-menu-max-width))]',
-            appStore.getCollapse
+            collapse.value
               ? 'left-[var(--left-menu-min-width)]'
-              : 'left-[var(--left-menu-max-width)]'
+              : 'left-[var(--left-menu-max-width)]',
+            '<md:(!left-0 !w-[100%])'
           ]}
         >
           <div
             class={[
-              `${perFixCls}-right__tool`,
+              'v-app-right__tool',
               'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
             ]}
           >
             <div class="h-full flex items-center">
               <Collapse class="header__tigger"></Collapse>
+              <Breadcrumb class="<md:hidden"></Breadcrumb>
             </div>
             <div class="h-full flex items-center">
               <Screenfull class="header__tigger"></Screenfull>
@@ -61,6 +71,9 @@ export default defineComponent({
               <UserInfo class="header__tigger"></UserInfo>
             </div>
           </div>
+          <div class="v-app-right__tags relative">
+            <TagsView></TagsView>
+          </div>
           <router-view>
             {{
               default: ({ Component, route }) => (
@@ -97,7 +110,8 @@ export default defineComponent({
   &-right {
     transition: left var(--transition-time-02);
 
-    &__tool {
+    &__tool,
+    &__tags {
       &::after {
         position: absolute;
         bottom: 0;

+ 1 - 1
src/main.ts

@@ -31,7 +31,7 @@ import App from './App.vue'
 
 import './permission'
 
-async function setupAll() {
+const setupAll = async () => {
   const app = createApp(App)
 
   await setupI18n(app)

+ 1 - 1
src/plugins/elementPlus/index.ts

@@ -7,7 +7,7 @@ const plugins = [ElLoading]
 
 const components = [ElScrollbar]
 
-export function setupElementPlus(app: App) {
+export const setupElementPlus = (app: App) => {
   plugins.forEach((plugin) => {
     app.use(plugin)
   })

+ 1 - 1
src/plugins/vueI18n/helper.ts

@@ -1,3 +1,3 @@
-export function setHtmlPageLang(locale: LocaleType) {
+export const setHtmlPageLang = (locale: LocaleType) => {
   document.querySelector('html')?.setAttribute('lang', locale)
 }

+ 2 - 2
src/plugins/vueI18n/index.ts

@@ -6,7 +6,7 @@ import { setHtmlPageLang } from './helper'
 
 export let i18n: ReturnType<typeof createI18n>
 
-async function createI18nOptions(): Promise<I18nOptions> {
+const createI18nOptions = async (): Promise<I18nOptions> => {
   const localeStore = useLocaleStoreWithOut()
   const locale = localeStore.getLocale
   const localeMap = localeStore.getLocaleMap
@@ -35,7 +35,7 @@ async function createI18nOptions(): Promise<I18nOptions> {
   }
 }
 
-export async function setupI18n(app: App) {
+export const setupI18n = async (app: App) => {
   const options = await createI18nOptions()
   i18n = createI18n(options) as I18n
   app.use(i18n)

+ 2 - 2
src/router/index.ts

@@ -121,7 +121,7 @@ const router = createRouter({
   scrollBehavior: () => ({ left: 0, top: 0 })
 })
 
-export function resetRouter(): void {
+export const resetRouter = (): void => {
   const resetWhiteNameList = ['RedirectRoot', 'Redirect', 'Login', 'Root', 'Dashboard', 'Page404']
   router.getRoutes().forEach((route) => {
     const { name } = route
@@ -131,7 +131,7 @@ export function resetRouter(): void {
   })
 }
 
-export function setupRouter(app: App<Element>) {
+export const setupRouter = (app: App<Element>) => {
   app.use(router)
 }
 

+ 1 - 1
src/store/index.ts

@@ -3,7 +3,7 @@ import { createPinia } from 'pinia'
 
 const store = createPinia()
 
-export function setupStore(app: App<Element>) {
+export const setupStore = (app: App<Element>) => {
   app.use(store)
 }
 

+ 7 - 1
src/store/modules/app.ts

@@ -66,6 +66,9 @@ export const useAppStore = defineStore({
     },
     getSizeMap(): ElememtPlusSzie[] {
       return this.sizeMap
+    },
+    getMobile(): boolean {
+      return this.mobile
     }
   },
   actions: {
@@ -128,10 +131,13 @@ export const useAppStore = defineStore({
     setSize(size: ElememtPlusSzie) {
       this.size = size
       wsCache.set('size', this.size)
+    },
+    setMobile(mobile: boolean) {
+      this.mobile = mobile
     }
   }
 })
 
-export function useAppStoreWithOut() {
+export const useAppStoreWithOut = () => {
   return useAppStore(store)
 }

+ 1 - 1
src/store/modules/locale.ts

@@ -27,6 +27,6 @@ export const useLocaleStore = defineStore({
   }
 })
 
-export function useLocaleStoreWithOut() {
+export const useLocaleStoreWithOut = () => {
   return useLocaleStore(store)
 }

+ 1 - 1
src/store/modules/permission.ts

@@ -91,6 +91,6 @@ export const usePermissionStore = defineStore({
   }
 })
 
-export function usePermissionStoreWithOut() {
+export const usePermissionStoreWithOut = () => {
   return usePermissionStore(store)
 }

+ 1 - 1
src/store/modules/tagsView.ts

@@ -170,6 +170,6 @@ export const useTagsViewStore = defineStore({
   }
 })
 
-export function useTagsViewStoreWithOut() {
+export const useTagsViewStoreWithOut = () => {
   return useTagsViewStore(store)
 }

+ 11 - 11
src/utils/color.ts

@@ -5,7 +5,7 @@
  * @param   String  color   十六进制颜色值
  * @return  Boolean
  */
-export function isHexColor(color: string) {
+export const isHexColor = (color: string) => {
   const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/
   return reg.test(color)
 }
@@ -19,7 +19,7 @@ export function isHexColor(color: string) {
  * @param g
  * @param b
  */
-export function rgbToHex(r: number, g: number, b: number) {
+export const rgbToHex = (r: number, g: number, b: number) => {
   // tslint:disable-next-line:no-bitwise
   const hex = ((r << 16) | (g << 8) | b).toString(16)
   return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex
@@ -30,7 +30,7 @@ export function rgbToHex(r: number, g: number, b: number) {
  * @param {string} hex The color to transform
  * @returns The RGB representation of the passed color
  */
-export function hexToRGB(hex: string) {
+export const hexToRGB = (hex: string) => {
   let sHex = hex.toLowerCase()
   if (isHexColor(hex)) {
     if (sHex.length === 4) {
@@ -49,7 +49,7 @@ export function hexToRGB(hex: string) {
   return sHex
 }
 
-export function colorIsDark(color: string) {
+export const colorIsDark = (color: string) => {
   if (!isHexColor(color)) return
   const [r, g, b] = hexToRGB(color)
     .replace(/(?:\(|\)|rgb|RGB)*/g, '')
@@ -64,7 +64,7 @@ export function colorIsDark(color: string) {
  * @param {number} amount The amount to change the color by
  * @returns {string} The HEX representation of the processed color
  */
-export function darken(color: string, amount: number) {
+export const darken = (color: string, amount: number) => {
   color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
   amount = Math.trunc((255 * amount) / 100)
   return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(
@@ -79,7 +79,7 @@ export function darken(color: string, amount: number) {
  * @param {number} amount The amount to change the color by
  * @returns {string} The processed color represented as HEX
  */
-export function lighten(color: string, amount: number) {
+export const lighten = (color: string, amount: number) => {
   color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
   amount = Math.trunc((255 * amount) / 100)
   return `#${addLight(color.substring(0, 2), amount)}${addLight(
@@ -95,7 +95,7 @@ export function lighten(color: string, amount: number) {
  * @param {number} amount The amount to change the color by
  * @returns {string} The processed part of the color
  */
-function addLight(color: string, amount: number) {
+const addLight = (color: string, amount: number) => {
   const cc = parseInt(color, 16) + amount
   const c = cc > 255 ? 255 : cc
   return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
@@ -107,7 +107,7 @@ function addLight(color: string, amount: number) {
  * @param {number} g green
  * @param {number} b blue
  */
-function luminanace(r: number, g: number, b: number) {
+const luminanace = (r: number, g: number, b: number) => {
   const a = [r, g, b].map((v) => {
     v /= 255
     return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
@@ -120,7 +120,7 @@ function luminanace(r: number, g: number, b: number) {
  * @param {string} rgb1 rgb color 1
  * @param {string} rgb2 rgb color 2
  */
-function contrast(rgb1: string[], rgb2: number[]) {
+const contrast = (rgb1: string[], rgb2: number[]) => {
   return (
     (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
     (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05)
@@ -131,7 +131,7 @@ function contrast(rgb1: string[], rgb2: number[]) {
  * Determines what the best text color is (black or white) based con the contrast with the background
  * @param hexColor - Last selected color by the user
  */
-export function calculateBestTextColor(hexColor: string) {
+export const calculateBestTextColor = (hexColor: string) => {
   const rgbColor = hexToRGB(hexColor.substring(1))
   const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0])
 
@@ -144,7 +144,7 @@ export function calculateBestTextColor(hexColor: string) {
  * @param {number} amount The amount to change the color by
  * @returns {string} The processed part of the color
  */
-function subtractLight(color: string, amount: number) {
+const subtractLight = (color: string, amount: number) => {
   const cc = parseInt(color, 16) - amount
   const c = cc < 0 ? 0 : cc
   return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`

+ 4 - 4
src/utils/index.ts

@@ -21,7 +21,7 @@ export const withInstall = <T>(component: T, alias?: string) => {
  * @param str 需要转下划线的驼峰字符串
  * @returns 字符串下划线
  */
-export function humpToUnderline(str: string): string {
+export const humpToUnderline = (str: string): string => {
   return str.replace(/([A-Z])/g, '-$1').toLowerCase()
 }
 
@@ -29,12 +29,12 @@ export function humpToUnderline(str: string): string {
  * @param str 需要转驼峰的下划线字符串
  * @returns 字符串驼峰
  */
-export function underlineToHump(str: string): string {
-  return str.replace(/\-(\w)/g, function (_, letter: string) {
+export const underlineToHump = (str: string): string => {
+  return str.replace(/\-(\w)/g, (_, letter: string) => {
     return letter.toUpperCase()
   })
 }
 
-export function setCssVar(prop: string, val: any, dom = document.documentElement) {
+export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
   dom.style.setProperty(prop, val)
 }

+ 21 - 21
src/utils/is.ts

@@ -2,23 +2,23 @@
 
 const toString = Object.prototype.toString
 
-export function is(val: unknown, type: string) {
+export const is = (val: unknown, type: string) => {
   return toString.call(val) === `[object ${type}]`
 }
 
-export function isDef<T = unknown>(val?: T): val is T {
+export const isDef = <T = unknown>(val?: T): val is T => {
   return typeof val !== 'undefined'
 }
 
-export function isUnDef<T = unknown>(val?: T): val is T {
+export const isUnDef = <T = unknown>(val?: T): val is T => {
   return !isDef(val)
 }
 
-export function isObject(val: any): val is Record<any, any> {
+export const isObject = (val: any): val is Record<any, any> => {
   return val !== null && is(val, 'Object')
 }
 
-export function isEmpty<T = unknown>(val: T): val is T {
+export const isEmpty = <T = unknown>(val: T): val is T => {
   if (isArray(val) || isString(val)) {
     return val.length === 0
   }
@@ -34,59 +34,59 @@ export function isEmpty<T = unknown>(val: T): val is T {
   return false
 }
 
-export function isDate(val: unknown): val is Date {
+export const isDate = (val: unknown): val is Date => {
   return is(val, 'Date')
 }
 
-export function isNull(val: unknown): val is null {
+export const isNull = (val: unknown): val is null => {
   return val === null
 }
 
-export function isNullAndUnDef(val: unknown): val is null | undefined {
+export const isNullAndUnDef = (val: unknown): val is null | undefined => {
   return isUnDef(val) && isNull(val)
 }
 
-export function isNullOrUnDef(val: unknown): val is null | undefined {
+export const isNullOrUnDef = (val: unknown): val is null | undefined => {
   return isUnDef(val) || isNull(val)
 }
 
-export function isNumber(val: unknown): val is number {
+export const isNumber = (val: unknown): val is number => {
   return is(val, 'Number')
 }
 
-export function isPromise<T = any>(val: unknown): val is Promise<T> {
+export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
   return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
 }
 
-export function isString(val: unknown): val is string {
+export const isString = (val: unknown): val is string => {
   return is(val, 'String')
 }
 
-export function isFunction(val: unknown): val is Function {
+export const isFunction = (val: unknown): val is Function => {
   return typeof val === 'function'
 }
 
-export function isBoolean(val: unknown): val is boolean {
+export const isBoolean = (val: unknown): val is boolean => {
   return is(val, 'Boolean')
 }
 
-export function isRegExp(val: unknown): val is RegExp {
+export const isRegExp = (val: unknown): val is RegExp => {
   return is(val, 'RegExp')
 }
 
-export function isArray(val: any): val is Array<any> {
+export const isArray = (val: any): val is Array<any> => {
   return val && Array.isArray(val)
 }
 
-export function isWindow(val: any): val is Window {
+export const isWindow = (val: any): val is Window => {
   return typeof window !== 'undefined' && is(val, 'Window')
 }
 
-export function isElement(val: unknown): val is Element {
+export const isElement = (val: unknown): val is Element => {
   return isObject(val) && !!val.tagName
 }
 
-export function isMap(val: unknown): val is Map<any, any> {
+export const isMap = (val: unknown): val is Map<any, any> => {
   return is(val, 'Map')
 }
 
@@ -94,12 +94,12 @@ export const isServer = typeof window === 'undefined'
 
 export const isClient = !isServer
 
-export function isUrl(path: string): boolean {
+export const isUrl = (path: string): boolean => {
   const reg =
     /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
   return reg.test(path)
 }
 
-export function isDark(): boolean {
+export const isDark = (): boolean => {
   return window.matchMedia('(prefers-color-scheme: dark)').matches
 }

+ 15 - 14
src/utils/routerHelper.ts

@@ -1,5 +1,5 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
-import type { Router, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
+import type { Router, RouteLocationNormalized, RouteRecordNormalized, RouteMeta } from 'vue-router'
 import { isUrl } from '@/utils/is'
 import { useCache } from '@/hooks/web/useCache'
 import { useAppStoreWithOut } from '@/store/modules/app'
@@ -23,7 +23,7 @@ export const getParentLayout = () => {
     })
 }
 
-export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
+export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => {
   if (!route) return route
   const { matched, ...opt } = route
   return {
@@ -39,15 +39,16 @@ export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormal
 }
 
 // 前端控制路由生成
-export function generateRoutesFn1(
+export const generateRoutesFn1 = (
   routes: AppRouteRecordRaw[],
   basePath = '/'
-): AppRouteRecordRaw[] {
+): AppRouteRecordRaw[] => {
   const res: AppRouteRecordRaw[] = []
 
   for (const route of routes) {
+    const meta = route.meta as RouteMeta
     // skip some route
-    if (route.meta && route.meta.hidden && !route.meta.showMainRoute) {
+    if (meta.hidden && !meta.showMainRoute) {
       continue
     }
 
@@ -55,7 +56,7 @@ export function generateRoutesFn1(
 
     let onlyOneChild: Nullable<string> = null
 
-    if (route.children && route.children.length === 1 && !route.meta.alwaysShow) {
+    if (route.children && route.children.length === 1 && !meta.alwaysShow) {
       onlyOneChild = (
         isUrl(route.children[0].path)
           ? route.children[0].path
@@ -72,7 +73,7 @@ export function generateRoutesFn1(
         data = Object.assign({}, route)
       } else {
         const routePath = pathResolve(basePath, onlyOneChild || route.path)
-        if (routePath === item.path || (route.meta && route.meta.followRoute === item.path)) {
+        if (routePath === item.path || meta.followRoute === item.path) {
           data = Object.assign({}, route)
         }
       }
@@ -90,7 +91,7 @@ export function generateRoutesFn1(
 }
 
 // 后端控制路由生成
-export function generateRoutesFn2(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
+export const generateRoutesFn2 = (routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] => {
   const res: AppRouteRecordRaw[] = []
 
   for (const route of routes) {
@@ -121,13 +122,13 @@ export function generateRoutesFn2(routes: AppRouteRecordRaw[]): AppRouteRecordRa
   return res
 }
 
-export function pathResolve(parentPath: string, path: string) {
+export const pathResolve = (parentPath: string, path: string) => {
   const childPath = path.startsWith('/') || !path ? path : `/${path}`
   return `${parentPath}${childPath}`
 }
 
 // 路由降级
-export function flatMultiLevelRoutes(routes: AppRouteRecordRaw[]) {
+export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
   const modules: AppRouteRecordRaw[] = cloneDeep(routes)
   for (let index = 0; index < modules.length; index++) {
     const route = modules[index]
@@ -140,7 +141,7 @@ export function flatMultiLevelRoutes(routes: AppRouteRecordRaw[]) {
 }
 
 // 层级是否大于2
-function isMultipleRoute(route: AppRouteRecordRaw) {
+const isMultipleRoute = (route: AppRouteRecordRaw) => {
   if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
     return false
   }
@@ -159,7 +160,7 @@ function isMultipleRoute(route: AppRouteRecordRaw) {
 }
 
 // 生成二级路由
-function promoteRouteLevel(route: AppRouteRecordRaw) {
+const promoteRouteLevel = (route: AppRouteRecordRaw) => {
   let router: Router | null = createRouter({
     routes: [route as unknown as RouteRecordNormalized],
     history: createWebHashHistory()
@@ -173,11 +174,11 @@ function promoteRouteLevel(route: AppRouteRecordRaw) {
 }
 
 // 添加所有子菜单
-function addToChildren(
+const addToChildren = (
   routes: RouteRecordNormalized[],
   children: AppRouteRecordRaw[],
   routeModule: AppRouteRecordRaw
-) {
+) => {
   for (let index = 0; index < children.length; index++) {
     const child = children[index]
     const route = routes.find((item) => item.name === child.name)

+ 207 - 0
src/utils/tree.ts

@@ -0,0 +1,207 @@
+interface TreeHelperConfig {
+  id: string
+  children: string
+  pid: string
+}
+const DEFAULT_CONFIG: TreeHelperConfig = {
+  id: 'id',
+  children: 'children',
+  pid: 'pid'
+}
+
+const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
+
+// tree from list
+export const listToTree = <T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] => {
+  const conf = getConfig(config) as TreeHelperConfig
+  const nodeMap = new Map()
+  const result: T[] = []
+  const { id, children, pid } = conf
+
+  for (const node of list) {
+    node[children] = node[children] || []
+    nodeMap.set(node[id], node)
+  }
+  for (const node of list) {
+    const parent = nodeMap.get(node[pid])
+    ;(parent ? parent.children : result).push(node)
+  }
+  return result
+}
+
+export const treeToList = <T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T => {
+  config = getConfig(config)
+  const { children } = config
+  const result: any = [...tree]
+  for (let i = 0; i < result.length; i++) {
+    if (!result[i][children!]) continue
+    result.splice(i + 1, 0, ...result[i][children!])
+  }
+  return result
+}
+
+export const findNode = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T | null => {
+  config = getConfig(config)
+  const { children } = config
+  const list = [...tree]
+  for (const node of list) {
+    if (func(node)) return node
+    node[children!] && list.push(...node[children!])
+  }
+  return null
+}
+
+export const findNodeAll = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T[] => {
+  config = getConfig(config)
+  const { children } = config
+  const list = [...tree]
+  const result: T[] = []
+  for (const node of list) {
+    func(node) && result.push(node)
+    node[children!] && list.push(...node[children!])
+  }
+  return result
+}
+
+export const findPath = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T | T[] | null => {
+  config = getConfig(config)
+  const path: T[] = []
+  const list = [...tree]
+  const visitedSet = new Set()
+  const { children } = config
+  while (list.length) {
+    const node = list[0]
+    if (visitedSet.has(node)) {
+      path.pop()
+      list.shift()
+    } else {
+      visitedSet.add(node)
+      node[children!] && list.unshift(...node[children!])
+      path.push(node)
+      if (func(node)) {
+        return path
+      }
+    }
+  }
+  return null
+}
+
+export const findPathAll = (tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) => {
+  config = getConfig(config)
+  const path: any[] = []
+  const list = [...tree]
+  const result: any[] = []
+  const visitedSet = new Set(),
+    { children } = config
+  while (list.length) {
+    const node = list[0]
+    if (visitedSet.has(node)) {
+      path.pop()
+      list.shift()
+    } else {
+      visitedSet.add(node)
+      node[children!] && list.unshift(...node[children!])
+      path.push(node)
+      func(node) && result.push([...path])
+    }
+  }
+  return result
+}
+
+export const filter = <T = any>(
+  tree: T[],
+  func: (n: T) => boolean,
+  config: Partial<TreeHelperConfig> = {}
+): T[] => {
+  config = getConfig(config)
+  const children = config.children as string
+  function listFilter(list: T[]) {
+    return list
+      .map((node: any) => ({ ...node }))
+      .filter((node) => {
+        node[children] = node[children] && listFilter(node[children])
+        return func(node) || (node[children] && node[children].length)
+      })
+  }
+  return listFilter(tree)
+}
+
+export const forEach = <T = any>(
+  tree: T[],
+  func: (n: T) => any,
+  config: Partial<TreeHelperConfig> = {}
+): void => {
+  config = getConfig(config)
+  const list: any[] = [...tree]
+  const { children } = config
+  for (let i = 0; i < list.length; i++) {
+    //func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
+    if (func(list[i])) {
+      return
+    }
+    children && list[i][children] && list.splice(i + 1, 0, ...list[i][children])
+  }
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMap = <T = any>(
+  treeData: T[],
+  opt: { children?: string; conversion: Fn }
+): T[] => {
+  return treeData.map((item) => treeMapEach(item, opt))
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMapEach = (
+  data: any,
+  { children = 'children', conversion }: { children?: string; conversion: Fn }
+) => {
+  const haveChildren = Array.isArray(data[children]) && data[children].length > 0
+  const conversionData = conversion(data) || {}
+  if (haveChildren) {
+    return {
+      ...conversionData,
+      [children]: data[children].map((i: number) =>
+        treeMapEach(i, {
+          children,
+          conversion
+        })
+      )
+    }
+  } else {
+    return {
+      ...conversionData
+    }
+  }
+}
+
+/**
+ * 递归遍历树结构
+ * @param treeDatas 树
+ * @param callBack 回调
+ * @param parentNode 父节点
+ */
+export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => {
+  treeDatas.forEach((element) => {
+    const newNode = callBack(element, parentNode) || element
+    if (element.children) {
+      eachTree(element.children, callBack, newNode)
+    }
+  })
+}

+ 1 - 1
src/utils/tsxHelper.ts

@@ -1,7 +1,7 @@
 import { Slots } from 'vue'
 import { isFunction } from '@/utils/is'
 
-export function getSlot(slots: Slots, slot = 'default', data?: Recordable) {
+export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => {
   // Reflect.has 判断一个对象是否存在某个属性
   if (!slots || !Reflect.has(slots, slot)) {
     return null

+ 1 - 1
src/views/Login/components/LoginForm.vue

@@ -111,7 +111,7 @@ watch(
 )
 
 // 登录
-async function signIn() {
+const signIn = async () => {
   const formRef = unref(elFormRef)
   const validate = await formRef?.validate()?.catch(() => {})
   if (validate) {