Browse Source

feat(utils): Add color utils

陈凯龙 3 years ago
parent
commit
71dfba21c5

+ 3 - 2
.pnpm-debug.log

@@ -13,7 +13,8 @@
       "name": "pnpm",
       "message": "butterfly-admin@3.0.0 clean: `npx rimraf docs/node_modules && npx rimraf node_modules`\nExit status 1",
       "code": "ELIFECYCLE",
-      "stack": "pnpm: butterfly-admin@3.0.0 clean: `npx rimraf docs/node_modules && npx rimraf node_modules`\nExit status 1\n    at EventEmitter.<anonymous> (C:\\Users\\Saber\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.24.4\\node_modules\\pnpm\\dist\\pnpm.cjs:103873:20)\n    at EventEmitter.emit (node:events:365:28)\n    at ChildProcess.<anonymous> (C:\\Users\\Saber\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.24.4\\node_modules\\pnpm\\dist\\pnpm.cjs:91802:18)\n    at ChildProcess.emit (node:events:365:28)\n    at maybeClose (node:internal/child_process:1067:16)\n    at Process.ChildProcess._handle.onexit (node:internal/child_process:301:5)"
+      "stack": "pnpm: butterfly-admin@3.0.0 clean: `npx rimraf docs/node_modules && npx rimraf node_modules`\nExit status 1\n    at EventEmitter.<anonymous> (C:\\Users\\admin\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.25.1\\node_modules\\pnpm\\dist\\pnpm.cjs:104843:20)\n    at EventEmitter.emit (events.js:315:20)\n    at ChildProcess.<anonymous> (C:\\Users\\admin\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.25.1\\node_modules\\pnpm\\dist\\pnpm.cjs:91921:18)\n    at ChildProcess.emit (events.js:315:20)\n    at maybeClose (internal/child_process.js:1048:16)\n    at Process.ChildProcess._handle.onexit (internal/child_process.js:288:5)"
     }
-  }
+  },
+  "2 warn pnpm:global": " Local package.json exists, but node_modules missing, did you mean to install?"
 }

+ 7 - 7
package.json

@@ -30,12 +30,12 @@
     "@zxcvbn-ts/core": "^1.2.0",
     "animate.css": "^4.1.1",
     "axios": "^0.24.0",
-    "element-plus": "1.3.0-beta.2",
+    "element-plus": "1.3.0-beta.5",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
     "nprogress": "^0.2.0",
     "pinia": "^2.0.9",
-    "qs": "^6.10.2",
+    "qs": "^6.10.3",
     "vue": "3.2.26",
     "vue-i18n": "9.1.9",
     "vue-router": "^4.0.12",
@@ -43,17 +43,17 @@
     "web-storage-cache": "^1.1.1"
   },
   "devDependencies": {
-    "@commitlint/cli": "^16.0.1",
+    "@commitlint/cli": "^16.0.2",
     "@commitlint/config-conventional": "^16.0.0",
-    "@iconify/json": "^1.1.453",
+    "@iconify/json": "^1.1.454",
     "@intlify/vite-plugin-vue-i18n": "^3.2.1",
     "@purge-icons/generated": "^0.7.0",
     "@types/lodash-es": "^4.17.5",
     "@types/node": "^17.0.8",
     "@types/nprogress": "^0.2.0",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.9.0",
-    "@typescript-eslint/parser": "^5.9.0",
+    "@typescript-eslint/eslint-plugin": "^5.9.1",
+    "@typescript-eslint/parser": "^5.9.1",
     "@vitejs/plugin-vue": "^2.0.1",
     "@vitejs/plugin-vue-jsx": "^1.3.3",
     "autoprefixer": "^10.4.2",
@@ -84,7 +84,7 @@
     "vite-plugin-purge-icons": "^0.7.0",
     "vite-plugin-style-import": "^1.4.1",
     "vite-plugin-svg-icons": "^1.1.0",
-    "vite-plugin-windicss": "^1.6.1",
+    "vite-plugin-windicss": "^1.6.2",
     "vue-tsc": "^0.30.2",
     "windicss": "^3.4.2",
     "windicss-analysis": "^0.3.5"

File diff suppressed because it is too large
+ 321 - 193
pnpm-lock.yaml


+ 1 - 4
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -2,7 +2,6 @@
 import { computed, unref } from 'vue'
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
-import { useCssVar } from '@vueuse/core'
 import { useLocale } from '@/hooks/web/useLocale'
 
 const localeStore = useLocaleStore()
@@ -11,8 +10,6 @@ const langMap = computed(() => localeStore.getLocaleMap)
 
 const currentLang = computed(() => localeStore.getLocale)
 
-const textColor = useCssVar('--el-text-color-primary', document.documentElement)
-
 function setLang(lang: LocaleType) {
   if (lang === unref(currentLang).lang) return
   // 需要重新加载页面让整个语言多初始化
@@ -29,7 +26,7 @@ function setLang(lang: LocaleType) {
   <ElDropdown trigger="click" @command="setLang">
     <Icon
       icon="ion:language-sharp"
-      :color="textColor"
+      color="var(--el-text-color-primary)"
       class="cursor-pointer"
       :class="$attrs.class"
     />

+ 3 - 0
src/components/Menu/index.ts

@@ -0,0 +1,3 @@
+import Menu from './src/Menu.vue'
+
+export { Menu }

+ 143 - 0
src/components/Menu/src/Menu.vue

@@ -0,0 +1,143 @@
+<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'
+import { useRenderMenuItem } from './components/useRenderMenuItem'
+import { useRouter } from 'vue-router'
+import { isUrl } from '@/utils/is'
+import { lighten } from '@/utils/color'
+console.log(lighten('#001529', 6))
+
+export default defineComponent({
+  name: 'Menu',
+  setup() {
+    const appStore = useAppStore()
+
+    const { push, currentRoute } = useRouter()
+
+    const permissionStore = usePermissionStore()
+
+    const { getPrefixCls } = useDesign()
+
+    const preFixCls = getPrefixCls('menu')
+
+    const menuMode = computed(() => {
+      // 水平模式
+      const vertical: LayoutType[] = ['classic']
+
+      if (vertical.includes(appStore.getLayout)) {
+        return 'vertical'
+      } else {
+        return 'horizontal'
+      }
+    })
+
+    const routers = computed(() => permissionStore.getRouters)
+
+    const collapse = computed(() => appStore.getCollapse)
+
+    const activeMenu = computed(() => {
+      const { meta, path } = currentRoute.value
+      // if set path, the sidebar will highlight the path you set
+      if (meta.activeMenu) {
+        return meta.activeMenu as string
+      }
+      return path
+    })
+
+    function menuSelect(index: string) {
+      if (isUrl(index)) {
+        window.open(index)
+      } else {
+        push(index)
+      }
+    }
+
+    return () => (
+      <div
+        class={[
+          preFixCls,
+          'h-[100%] overflow-hidden',
+          appStore.getCollapse
+            ? 'w-[var(--left-menu-min-width)]'
+            : 'w-[var(--left-menu-max-width)]',
+          'bg-[var(--left-menu-bg-color)]'
+        ]}
+      >
+        <ElScrollbar>
+          <ElMenu
+            defaultActive={activeMenu.value}
+            mode={menuMode.value}
+            collapse={collapse.value}
+            backgroundColor="var(--left-menu-bg-color)"
+            textColor="var(--left-menu-text-color)"
+            activeTextColor="var(--left-menu-text-active-color)"
+            onSelect={menuSelect}
+          >
+            {{
+              default: () => {
+                const { renderMenuItem } = useRenderMenuItem(routers.value)
+                return renderMenuItem()
+              }
+            }}
+          </ElMenu>
+        </ElScrollbar>
+      </div>
+    )
+  }
+})
+</script>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-menu';
+
+@menuBgColor: var(--left-menu-bg-color);
+
+.@{prefix-cls} {
+  :deep(.el-menu) {
+    border-right: none;
+
+    // 设置选中时子标题的颜色
+    .is-active {
+      & > .el-sub-menu__title {
+        color: var(--left-menu-text-active-color) !important;
+      }
+    }
+
+    // 设置子菜单悬停的高亮和背景色
+    .el-sub-menu__title,
+    .el-menu-item {
+      &:hover {
+        color: var(--left-menu-text-active-color) !important;
+        background-color: var(--left-menu-bg-color) !important;
+      }
+    }
+
+    // 设置选中时的高亮背景和高亮颜色
+    .el-sub-menu.is-active,
+    .el-menu-item.is-active {
+      color: var(--left-menu-text-active-color) !important;
+      background-color: var(--left-menu-bg-active-color) !important;
+
+      &:hover {
+        background-color: var(--left-menu-bg-active-color) !important;
+      }
+    }
+
+    // 设置子菜单的背景颜色
+    .el-menu {
+      .el-sub-menu__title,
+      .el-menu-item:not(.is-active) {
+        background-color: var(--left-menu-bg-light-color) !important;
+      }
+    }
+  }
+
+  :deep(.el-menu--collapse) {
+    width: var(--left-menu-min-width);
+  }
+}
+</style>

+ 49 - 0
src/components/Menu/src/components/useRenderMenuItem.tsx

@@ -0,0 +1,49 @@
+import { ElSubMenu, ElMenuItem } from 'element-plus'
+import type { RouteMeta } from 'vue-router'
+import { getAllParentPath, hasOneShowingChild } from '../helper'
+import { isUrl } from '@/utils/is'
+import { useRenderMenuTitle } from './useRenderMenuTitle'
+
+export function useRenderMenuItem(allRouters: AppRouteRecordRaw[] = []) {
+  function renderMenuItem(routers?: AppRouteRecordRaw[]) {
+    return (routers || allRouters).map((v) => {
+      const meta = (v.meta ?? {}) as RouteMeta
+      if (!meta.hidden) {
+        const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
+
+        const fullPath = isUrl(v.path)
+          ? v.path
+          : getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
+
+        const { renderMenuTitle } = useRenderMenuTitle()
+
+        if (
+          oneShowingChild &&
+          (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
+          !meta?.alwaysShow
+        ) {
+          return (
+            <ElMenuItem index={fullPath}>
+              {{
+                default: () => renderMenuTitle(meta)
+              }}
+            </ElMenuItem>
+          )
+        } else {
+          return (
+            <ElSubMenu index={fullPath}>
+              {{
+                title: () => renderMenuTitle(meta),
+                default: () => renderMenuItem(v.children)
+              }}
+            </ElSubMenu>
+          )
+        }
+      }
+    })
+  }
+
+  return {
+    renderMenuItem
+  }
+}

+ 23 - 0
src/components/Menu/src/components/useRenderMenuTitle.tsx

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

+ 95 - 0
src/components/Menu/src/helper.ts

@@ -0,0 +1,95 @@
+import type { RouteMeta } from 'vue-router'
+import { ref, unref } from 'vue'
+
+interface TreeConfig {
+  id: string
+  children: string
+  pid: string
+}
+
+type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
+
+interface HasOneShowingChild {
+  oneShowingChild?: boolean
+  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) {
+  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(
+  children: AppRouteRecordRaw[] = [],
+  parent: AppRouteRecordRaw
+): HasOneShowingChild {
+  const onlyOneChild = ref<OnlyOneChildType>()
+
+  const showingChildren = children.filter((v) => {
+    const meta = (v.meta ?? {}) as RouteMeta
+    if (meta.hidden) {
+      return false
+    } else {
+      // Temp set(will be used if only has one showing child)
+      onlyOneChild.value = v
+      return true
+    }
+  })
+
+  // When there is only one child router, the child router is displayed by default
+  if (showingChildren.length === 1) {
+    return {
+      oneShowingChild: true,
+      onlyOneChild: unref(onlyOneChild)
+    }
+  }
+
+  // Show parent if there are no child router to display
+  if (!showingChildren.length) {
+    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+    return {
+      oneShowingChild: true,
+      onlyOneChild: unref(onlyOneChild)
+    }
+  }
+
+  return {
+    oneShowingChild: false,
+    onlyOneChild: unref(onlyOneChild)
+  }
+}

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

@@ -2,7 +2,6 @@
 import { computed } from 'vue'
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
 import { useAppStore } from '@/store/modules/app'
-import { useCssVar } from '@vueuse/core'
 import { useI18n } from '@/hooks/web/useI18n'
 const { t } = useI18n()
 
@@ -10,8 +9,6 @@ const appStore = useAppStore()
 
 const sizeMap = computed(() => appStore.sizeMap)
 
-const textColor = useCssVar('--el-text-color-primary', document.documentElement)
-
 function setSize(size: ElememtPlusSzie) {
   appStore.setSize(size)
 }
@@ -19,7 +16,7 @@ function setSize(size: ElememtPlusSzie) {
 
 <template>
   <ElDropdown trigger="click" @command="setSize">
-    <Icon icon="mdi:format-size" :color="textColor" class="cursor-pointer" />
+    <Icon icon="mdi:format-size" color="var(--el-text-color-primary)" class="cursor-pointer" />
     <template #dropdown>
       <ElDropdownMenu>
         <ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">

+ 1 - 2
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 { useCssVar } from '@vueuse/core'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useIcon } from '@/hooks/web/useIcon'
 
@@ -20,7 +19,7 @@ const { getPrefixCls } = useDesign()
 const prefixCls = getPrefixCls('theme-switch')
 
 // 设置switch的背景颜色
-const blackColor = useCssVar('--el-color-black', document.documentElement)
+const blackColor = 'var(--el-color-black)'
 
 function themeChange(val: boolean) {
   appStore.setIsDark(val)

+ 4 - 4
src/config/app.ts

@@ -2,10 +2,10 @@ import { useCache } from '@/hooks/web/useCache'
 
 const { wsCache } = useCache()
 
-export type LayoutType = 'Classic' | 'LeftTop' | 'Top' | 'Test'
+export type LayoutType = 'classic' | 'leftTop' | 'top' | 'test'
 
 export interface AppState {
-  collapsed: boolean
+  collapse: boolean
   showTags: boolean
   showLogo: boolean
   showNavbar: boolean
@@ -27,12 +27,12 @@ export interface AppState {
 }
 
 export const appModules: AppState = {
-  collapsed: false, // 菜单栏是否栏缩收
+  collapse: false, // 菜单栏是否栏缩收
   showLogo: true, // 是否显示logo
   showTags: true, // 是否显示标签栏
   showNavbar: true, // 是否显示navbar
   fixedHeader: true, // 是否固定header
-  layout: 'Classic', // layout布局
+  layout: 'classic', // layout布局
   showBreadcrumb: true, // 是否显示面包屑
   showHamburger: true, // 是否显示侧边栏缩收按钮
   showScreenfull: true, // 是否全屏按钮

+ 42 - 15
src/layout/Layout.vue

@@ -1,16 +1,43 @@
-<script setup lang="ts">
-// import { computed } from 'vue'
-// const getCaches = computed((): string[] => {
-//   return []
-// })
-</script>
+<script lang="tsx">
+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'
+
+const tagsViewStore = useTagsViewStore()
+
+const getCaches = computed((): string[] => {
+  return tagsViewStore.getCachedViews
+})
+
+const appStore = useAppStore()
+console.log(appStore)
+const classSuffix = computed(() => appStore.getLayout)
 
-<template>
-  <RouterView>
-    <template #default="{ Component, route }">
-      <KeepAlive>
-        <Component :is="Component" :key="route.fullPath" />
-      </KeepAlive>
-    </template>
-  </RouterView>
-</template>
+const { getPrefixCls } = useDesign()
+
+const perFixCls = getPrefixCls('app')
+
+export default defineComponent({
+  name: 'Layout',
+  setup() {
+    return () => (
+      <section
+        class={[perFixCls, `${perFixCls}__${classSuffix.value}`, 'w-[100%] h-[100%] relative']}
+      >
+        <Menu></Menu>
+        <router-view class="absolute top-0 right-0 ">
+          {{
+            default: ({ Component, route }) => (
+              <KeepAlive include={getCaches.value}>
+                <Component is={Component} key={route.fullPath}></Component>
+              </KeepAlive>
+            )
+          }}
+        </router-view>
+      </section>
+    )
+  }
+})
+</script>

+ 3 - 5
src/router/index.ts

@@ -1,14 +1,11 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
 import type { RouteRecordRaw } from 'vue-router'
 import type { App } from 'vue'
-import { getParentLayout } from '@/utils/routerHelper'
+import { Layout, getParentLayout } from '@/utils/routerHelper'
 import { useI18n } from '@/hooks/web/useI18n'
 
 const { t } = useI18n()
 
-/* Layout */
-const Layout = () => import('@/layout/Layout.vue')
-
 export const constantRouterMap: AppRouteRecordRaw[] = [
   {
     path: '/redirect',
@@ -45,7 +42,8 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
     redirect: '/level/menu1/menu1-1/menu1-1-1',
     name: 'Level',
     meta: {
-      title: t('router.level')
+      title: t('router.level'),
+      icon: 'carbon:skill-level-advanced'
     },
     children: [
       {

+ 4 - 4
src/store/modules/app.ts

@@ -10,8 +10,8 @@ export const useAppStore = defineStore({
   id: 'app',
   state: (): AppState => appModules,
   getters: {
-    getCollapsed(): boolean {
-      return this.collapsed
+    getCollapse(): boolean {
+      return this.collapse
     },
     getShowLogo(): boolean {
       return this.showLogo
@@ -69,8 +69,8 @@ export const useAppStore = defineStore({
     }
   },
   actions: {
-    setCollapsed(collapsed: boolean) {
-      this.collapsed = collapsed
+    setCollapse(collapse: boolean) {
+      this.collapse = collapse
     },
     setShowLogo(showLogo: boolean) {
       this.showLogo = showLogo

+ 175 - 0
src/store/modules/tagsView.ts

@@ -0,0 +1,175 @@
+// import router from '@/router'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+import { getRawRoute } from '@/utils/routerHelper'
+import { defineStore } from 'pinia'
+import { store } from '../index'
+
+export interface TagsViewState {
+  visitedViews: RouteLocationNormalizedLoaded[]
+  cachedViews: Set<string>
+}
+
+export const useTagsViewStore = defineStore({
+  id: 'tagsView',
+  state: (): TagsViewState => ({
+    visitedViews: [],
+    cachedViews: new Set()
+  }),
+  getters: {
+    getVisitedViews(): RouteLocationNormalizedLoaded[] {
+      return this.visitedViews
+    },
+    getCachedViews(): string[] {
+      return Array.from(this.cachedViews)
+    }
+  },
+  actions: {
+    ADD_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void {
+      if (this.visitedViews.some((v: RouteLocationNormalizedLoaded) => v.path === view.path)) return
+      if (view.meta?.noTagsView) return
+      this.visitedViews.push(
+        Object.assign({}, view, {
+          title: view.meta.title || 'no-name'
+        })
+      )
+    },
+    SET_CACHED_VIEW(): void {
+      const cacheMap: Set<string> = new Set()
+
+      for (const v of this.visitedViews) {
+        const item = getRawRoute(v)
+        const needCache = !item.meta?.noCache
+        if (!needCache) {
+          continue
+        }
+        const name = item.name as string
+        cacheMap.add(name)
+      }
+      this.cachedViews = cacheMap
+    },
+    DEL_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void {
+      for (const [i, v] of this.visitedViews.entries()) {
+        if (v.path === view.path) {
+          this.visitedViews.splice(i, 1)
+          break
+        }
+      }
+    },
+    DEL_CACHED_VIEW(): void {
+      // const route = router.currentRoute.value
+      // for (const [key, value] of this.cachedViews) {
+      //   const index = value.findIndex((item: string) => item === (route.name as string))
+      //   if (index === -1) {
+      //     continue
+      //   }
+      //   if (value.length === 1) {
+      //     this.cachedViews.delete(key)
+      //     continue
+      //   }
+      //   value.splice(index, 1)
+      //   this.cachedViews.set(key, value)
+      // }
+    },
+    DEL_OTHERS_VISITED_VIEWS(view: RouteLocationNormalizedLoaded): void {
+      this.visitedViews = this.visitedViews.filter((v) => {
+        return v.meta.affix || v.path === view.path
+      })
+    },
+    DEL_ALL_VISITED_VIEWS(): void {
+      // keep affix tags
+      const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
+      this.visitedViews = affixTags
+    },
+    UPDATE_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void {
+      for (let v of this.visitedViews) {
+        if (v.path === view.path) {
+          v = Object.assign(v, view)
+          break
+        }
+      }
+    },
+    addView(view: RouteLocationNormalizedLoaded): void {
+      this.addVisitedView(view)
+      this.addCachedView()
+    },
+    addVisitedView(view: RouteLocationNormalizedLoaded): void {
+      this.ADD_VISITED_VIEW(view)
+    },
+    addCachedView(): void {
+      this.SET_CACHED_VIEW()
+    },
+    delView(view: RouteLocationNormalizedLoaded): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.delVisitedView(view)
+        this.SET_CACHED_VIEW()
+        resolve({
+          visitedViews: [...this.visitedViews],
+          cachedViews: [...this.cachedViews]
+        })
+      })
+    },
+    delVisitedView(view: RouteLocationNormalizedLoaded): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.DEL_VISITED_VIEW(view)
+        resolve([...this.visitedViews])
+      })
+    },
+    delCachedView(): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.DEL_CACHED_VIEW()
+        resolve([...this.cachedViews])
+      })
+    },
+    delOthersViews(view: RouteLocationNormalizedLoaded): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.delOthersVisitedViews(view)
+        this.SET_CACHED_VIEW()
+        resolve({
+          visitedViews: [...this.visitedViews],
+          cachedViews: [...this.cachedViews]
+        })
+      })
+    },
+    delOthersVisitedViews(view: RouteLocationNormalizedLoaded): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.DEL_OTHERS_VISITED_VIEWS(view)
+        resolve([...this.visitedViews])
+      })
+    },
+    delOthersCachedViews(): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.SET_CACHED_VIEW()
+        resolve([...this.cachedViews])
+      })
+    },
+    delAllViews(): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.delAllVisitedViews()
+        this.SET_CACHED_VIEW()
+        resolve({
+          visitedViews: [...this.visitedViews],
+          cachedViews: [...this.cachedViews]
+        })
+      })
+    },
+    delAllVisitedViews(): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.DEL_ALL_VISITED_VIEWS()
+        resolve([...this.visitedViews])
+      })
+    },
+    delAllCachedViews(): Promise<unknown> {
+      return new Promise((resolve) => {
+        this.SET_CACHED_VIEW()
+        resolve([...this.cachedViews])
+      })
+    },
+    updateVisitedView(view: RouteLocationNormalizedLoaded): void {
+      this.UPDATE_VISITED_VIEW(view)
+    }
+  }
+})
+
+export function useTagsViewStoreWithOut() {
+  return useTagsViewStore(store)
+}

+ 14 - 0
src/styles/var.css

@@ -1,3 +1,17 @@
 :root {
   --dark-bg-color: #293146;
+
+  --left-menu-max-width: 200px;
+
+  --left-menu-min-width: 64px;
+
+  --left-menu-bg-color: #001529;
+
+  --left-menu-bg-light-color: #0f2438;
+
+  --left-menu-bg-active-color: var(--el-color-primary);
+
+  --left-menu-text-color: #bfcbd9;
+
+  --left-menu-text-active-color: #fff;
 }

+ 155 - 0
src/utils/color.ts

@@ -0,0 +1,155 @@
+/**
+ * 判断是否 十六进制颜色值.
+ * 输入形式可为 #fff000 #f00
+ *
+ * @param   String  color   十六进制颜色值
+ * @return  Boolean
+ */
+export function isHexColor(color: string) {
+  const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/
+  return reg.test(color)
+}
+
+/**
+ * RGB 颜色值转换为 十六进制颜色值.
+ * r, g, 和 b 需要在 [0, 255] 范围内
+ *
+ * @return  String          类似#ff00ff
+ * @param r
+ * @param g
+ * @param b
+ */
+export function 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
+}
+
+/**
+ * Transform a HEX color to its RGB representation
+ * @param {string} hex The color to transform
+ * @returns The RGB representation of the passed color
+ */
+export function hexToRGB(hex: string) {
+  let sHex = hex.toLowerCase()
+  if (isHexColor(hex)) {
+    if (sHex.length === 4) {
+      let sColorNew = '#'
+      for (let i = 1; i < 4; i += 1) {
+        sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1))
+      }
+      sHex = sColorNew
+    }
+    const sColorChange: number[] = []
+    for (let i = 1; i < 7; i += 2) {
+      sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)))
+    }
+    return 'RGB(' + sColorChange.join(',') + ')'
+  }
+  return sHex
+}
+
+export function colorIsDark(color: string) {
+  if (!isHexColor(color)) return
+  const [r, g, b] = hexToRGB(color)
+    .replace(/(?:\(|\)|rgb|RGB)*/g, '')
+    .split(',')
+    .map((item) => Number(item))
+  return r * 0.299 + g * 0.578 + b * 0.114 < 192
+}
+
+/**
+ * Darkens a HEX color given the passed percentage
+ * @param {string} color The color to process
+ * @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) {
+  color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
+  amount = Math.trunc((255 * amount) / 100)
+  return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(
+    color.substring(2, 4),
+    amount
+  )}${subtractLight(color.substring(4, 6), amount)}`
+}
+
+/**
+ * Lightens a 6 char HEX color according to the passed percentage
+ * @param {string} color The color to change
+ * @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) {
+  color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
+  amount = Math.trunc((255 * amount) / 100)
+  return `#${addLight(color.substring(0, 2), amount)}${addLight(
+    color.substring(2, 4),
+    amount
+  )}${addLight(color.substring(4, 6), amount)}`
+}
+
+/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */
+/**
+ * Sums the passed percentage to the R, G or B of a HEX color
+ * @param {string} color The color to change
+ * @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 cc = parseInt(color, 16) + amount
+  const c = cc > 255 ? 255 : cc
+  return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
+}
+
+/**
+ * Calculates luminance of an rgb color
+ * @param {number} r red
+ * @param {number} g green
+ * @param {number} b blue
+ */
+function 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)
+  })
+  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722
+}
+
+/**
+ * Calculates contrast between two rgb colors
+ * @param {string} rgb1 rgb color 1
+ * @param {string} rgb2 rgb color 2
+ */
+function contrast(rgb1: string[], rgb2: number[]) {
+  return (
+    (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
+    (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05)
+  )
+}
+
+/**
+ * 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) {
+  const rgbColor = hexToRGB(hexColor.substring(1))
+  const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0])
+
+  return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'
+}
+
+/**
+ * Subtracts the indicated percentage to the R, G or B of a HEX color
+ * @param {string} color The color to change
+ * @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 cc = parseInt(color, 16) - amount
+  const c = cc < 0 ? 0 : cc
+  return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
+}
+
+export function setCssVar(prop: string, val: any, dom = document.documentElement) {
+  dom.style.setProperty(prop, val)
+}

+ 0 - 20
src/utils/index.ts

@@ -34,23 +34,3 @@ export function underlineToHump(str: string): string {
     return letter.toUpperCase()
   })
 }
-
-/**
- * 对象数组深拷贝
- * @param {Array,Object} source 需要深拷贝的对象数组
- * @param {Array} noClone 不需要深拷贝的属性集合
- */
-export function deepClone(source: any, noClone: string[] = []): any {
-  if (!source && typeof source !== 'object') {
-    throw new Error('error arguments deepClone')
-  }
-  const targetObj: any = source.constructor === Array ? [] : {}
-  Object.keys(source).forEach((keys: string) => {
-    if (source[keys] && typeof source[keys] === 'object' && noClone.indexOf(keys) === -1) {
-      targetObj[keys] = deepClone(source[keys], noClone)
-    } else {
-      targetObj[keys] = source[keys]
-    }
-  })
-  return targetObj
-}

+ 13 - 11
src/utils/routerHelper.ts

@@ -12,7 +12,7 @@ const { wsCache } = useCache()
 const modules = import.meta.glob('../../views/**/*.{vue,tsx}')
 
 /* Layout */
-const Layout = () => import('@/layout/index.vue')
+export const Layout = () => import('@/layout/Layout.vue')
 
 export const getParentLayout = () => {
   return () =>
@@ -23,7 +23,7 @@ export const getParentLayout = () => {
     })
 }
 
-export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized {
+export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
   if (!route) return route
   const { matched, ...opt } = route
   return {
@@ -101,14 +101,16 @@ export function generateRoutesFn2(routes: AppRouteRecordRaw[]): AppRouteRecordRa
       meta: route.meta
     }
     if (route.component) {
-      // 动态加载路由文件,可根据实际情况进行自定义逻辑
-      const component = route.component as string
-      data.component =
-        component === '#'
-          ? Layout
-          : component.includes('##')
-          ? getParentLayout()
-          : modules[`../../${route.component}.vue`] || modules[`../../${route.component}.tsx`]
+      const comModule =
+        modules[`../../${route.component}.vue`] || modules[`../../${route.component}.tsx`]
+      if (comModule) {
+        // 动态加载路由文件,可根据实际情况进行自定义逻辑
+        const component = route.component as string
+        data.component =
+          component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule
+      } else {
+        console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`)
+      }
     }
     // recursive child routes
     if (route.children) {
@@ -155,7 +157,7 @@ function isMultipleRoute(route: AppRouteRecordRaw) {
   return flag
 }
 
-// 路由降级
+// 生成二级路由
 function promoteRouteLevel(route: AppRouteRecordRaw) {
   let router: Router | null = createRouter({
     routes: [route as unknown as RouteRecordNormalized],

+ 0 - 1
types/router.d.ts

@@ -61,7 +61,6 @@ declare global {
     name: string
     meta: RouteMeta
     component?: Component | string
-    components?: Component
     children?: AppRouteRecordRaw[]
     props?: Recordable
     fullPath?: string

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