From e20fa76cad0894a69fd04c81c2108faabf392684 Mon Sep 17 00:00:00 2001
From: kailong321200875 <321200875@qq.com>
Date: Sat, 22 Jan 2022 19:39:44 +0800
Subject: [PATCH] feat(component): Add CountTo component and Echart component

---
 package.json                              |   2 +
 pnpm-lock.yaml                            | 130 +++++++++++-----
 src/components/CountTo/index.ts           |   0
 src/components/CountTo/src/CountTo.vue    | 175 ++++++++++++++++++++++
 src/components/Echart/index.ts            |   3 +
 src/components/Echart/src/Echart.vue      | 110 ++++++++++++++
 src/components/TagsView/src/TagsView.vue  |   4 +-
 src/layout/components/useRenderLayout.tsx |  12 +-
 src/locales/en.ts                         |   4 +-
 src/locales/zh-CN.ts                      |   4 +-
 src/permission.ts                         |   2 +-
 src/plugins/echarts/index.ts              |  33 ++++
 src/router/index.ts                       |  23 +++
 src/views/Dashboard/Analysis.vue          |   7 +
 src/views/Dashboard/echarts-data.ts       |   0
 15 files changed, 461 insertions(+), 48 deletions(-)
 create mode 100644 src/components/CountTo/index.ts
 create mode 100644 src/components/CountTo/src/CountTo.vue
 create mode 100644 src/components/Echart/index.ts
 create mode 100644 src/components/Echart/src/Echart.vue
 create mode 100644 src/plugins/echarts/index.ts
 create mode 100644 src/views/Dashboard/Analysis.vue
 create mode 100644 src/views/Dashboard/echarts-data.ts

diff --git a/package.json b/package.json
index 90fbfbb..754149b 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,8 @@
     "@zxcvbn-ts/core": "^1.2.0",
     "animate.css": "^4.1.1",
     "axios": "^0.25.0",
+    "echarts": "^5.2.2",
+    "echarts-wordcloud": "^2.0.0",
     "element-plus": "1.3.0-beta.5",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a9dd38e..dc3b93d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,8 @@ specifiers:
   autoprefixer: ^10.4.2
   axios: ^0.25.0
   commitizen: ^4.2.4
+  echarts: ^5.2.2
+  echarts-wordcloud: ^2.0.0
   element-plus: 1.3.0-beta.5
   eslint: ^8.7.0
   eslint-config-prettier: ^8.3.0
@@ -70,6 +72,8 @@ dependencies:
   '@zxcvbn-ts/core': registry.npmmirror.com/@zxcvbn-ts/core/1.2.0
   animate.css: registry.npmmirror.com/animate.css/4.1.1
   axios: registry.npmmirror.com/axios/0.25.0
+  echarts: registry.npmmirror.com/echarts/5.2.2
+  echarts-wordcloud: registry.npmmirror.com/echarts-wordcloud/2.0.0_echarts@5.2.2
   element-plus: registry.npmmirror.com/element-plus/1.3.0-beta.5_vue@3.2.26
   lodash-es: registry.nlark.com/lodash-es/4.17.21
   mockjs: registry.npmmirror.com/mockjs/1.1.0
@@ -350,38 +354,6 @@ packages:
     version: 1.0.2
     dev: true
 
-  registry.nlark.com/acorn-jsx/5.3.2_acorn@7.4.1:
-    resolution:
-      {
-        integrity: sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=,
-        registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/acorn-jsx/download/acorn-jsx-5.3.2.tgz
-      }
-    id: registry.nlark.com/acorn-jsx/5.3.2
-    name: acorn-jsx
-    version: 5.3.2
-    peerDependencies:
-      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-    dependencies:
-      acorn: registry.npmmirror.com/acorn/7.4.1
-    dev: true
-
-  registry.nlark.com/acorn-jsx/5.3.2_acorn@8.7.0:
-    resolution:
-      {
-        integrity: sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=,
-        registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.nlark.com/acorn-jsx/download/acorn-jsx-5.3.2.tgz
-      }
-    id: registry.nlark.com/acorn-jsx/5.3.2
-    name: acorn-jsx
-    version: 5.3.2
-    peerDependencies:
-      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-    dependencies:
-      acorn: registry.npmmirror.com/acorn/8.7.0
-    dev: true
-
   registry.nlark.com/acorn-walk/8.2.0:
     resolution:
       {
@@ -1781,7 +1753,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
@@ -6244,6 +6216,19 @@ packages:
     engines: { node: '>=10' }
     dev: true
 
+  registry.nlark.com/zrender/5.2.1:
+    resolution:
+      {
+        integrity: sha1-X0u9qRW6bUErCxncJDG+qtBUF7s=,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.nlark.com/zrender/download/zrender-5.2.1.tgz
+      }
+    name: zrender
+    version: 5.2.1
+    dependencies:
+      tslib: registry.npmmirror.com/tslib/2.3.0
+    dev: false
+
   registry.npmmirror.com/@antfu/utils/0.3.0:
     resolution:
       {
@@ -8402,6 +8387,38 @@ packages:
       through: registry.nlark.com/through/2.3.8
     dev: true
 
+  registry.npmmirror.com/acorn-jsx/5.3.2_acorn@7.4.1:
+    resolution:
+      {
+        integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/acorn-jsx/download/acorn-jsx-5.3.2.tgz
+      }
+    id: registry.npmmirror.com/acorn-jsx/5.3.2
+    name: acorn-jsx
+    version: 5.3.2
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+    dependencies:
+      acorn: registry.npmmirror.com/acorn/7.4.1
+    dev: true
+
+  registry.npmmirror.com/acorn-jsx/5.3.2_acorn@8.7.0:
+    resolution:
+      {
+        integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/acorn-jsx/download/acorn-jsx-5.3.2.tgz
+      }
+    id: registry.npmmirror.com/acorn-jsx/5.3.2
+    name: acorn-jsx
+    version: 5.3.2
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+    dependencies:
+      acorn: registry.npmmirror.com/acorn/8.7.0
+    dev: true
+
   registry.npmmirror.com/acorn/7.4.1:
     resolution:
       {
@@ -9378,6 +9395,36 @@ packages:
       domhandler: registry.npmmirror.com/domhandler/4.3.0
     dev: true
 
+  registry.npmmirror.com/echarts-wordcloud/2.0.0_echarts@5.2.2:
+    resolution:
+      {
+        integrity: sha512-K7l6pTklqdW7ZWzT/1CS0KhBSINr/cd7c5N1fVMzZMwLQHEwT7x+nivK7g5hkVh7WNcAv4Dn6/ZS5zMKRozC1g==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/echarts-wordcloud/download/echarts-wordcloud-2.0.0.tgz
+      }
+    id: registry.npmmirror.com/echarts-wordcloud/2.0.0
+    name: echarts-wordcloud
+    version: 2.0.0
+    peerDependencies:
+      echarts: ^5.0.1
+    dependencies:
+      echarts: registry.npmmirror.com/echarts/5.2.2
+    dev: false
+
+  registry.npmmirror.com/echarts/5.2.2:
+    resolution:
+      {
+        integrity: sha512-yxuBfeIH5c+0FsoRP60w4De6omXhA06c7eUYBsC1ykB6Ys2yK5fSteIYWvkJ4xJVLQgCvAdO8C4mN6MLeJpBaw==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/echarts/download/echarts-5.2.2.tgz
+      }
+    name: echarts
+    version: 5.2.2
+    dependencies:
+      tslib: registry.npmmirror.com/tslib/2.3.0
+      zrender: registry.nlark.com/zrender/5.2.1
+    dev: false
+
   registry.npmmirror.com/electron-to-chromium/1.4.30:
     resolution:
       {
@@ -10035,7 +10082,7 @@ packages:
     engines: { node: '>=6.0.0' }
     dependencies:
       acorn: registry.npmmirror.com/acorn/7.4.1
-      acorn-jsx: registry.nlark.com/acorn-jsx/5.3.2_acorn@7.4.1
+      acorn-jsx: registry.npmmirror.com/acorn-jsx/5.3.2_acorn@7.4.1
       eslint-visitor-keys: registry.npmmirror.com/eslint-visitor-keys/1.3.0
     dev: true
 
@@ -10051,7 +10098,7 @@ packages:
     engines: { node: ^10.12.0 || >=12.0.0 }
     dependencies:
       acorn: registry.npmmirror.com/acorn/7.4.1
-      acorn-jsx: registry.nlark.com/acorn-jsx/5.3.2_acorn@7.4.1
+      acorn-jsx: registry.npmmirror.com/acorn-jsx/5.3.2_acorn@7.4.1
       eslint-visitor-keys: registry.npmmirror.com/eslint-visitor-keys/1.3.0
     dev: true
 
@@ -10067,7 +10114,7 @@ packages:
     engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
     dependencies:
       acorn: registry.npmmirror.com/acorn/8.7.0
-      acorn-jsx: registry.nlark.com/acorn-jsx/5.3.2_acorn@8.7.0
+      acorn-jsx: registry.npmmirror.com/acorn-jsx/5.3.2_acorn@8.7.0
       eslint-visitor-keys: registry.npmmirror.com/eslint-visitor-keys/3.2.0
     dev: true
 
@@ -12239,6 +12286,17 @@ packages:
     version: 1.14.1
     dev: true
 
+  registry.npmmirror.com/tslib/2.3.0:
+    resolution:
+      {
+        integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/tslib/download/tslib-2.3.0.tgz
+      }
+    name: tslib
+    version: 2.3.0
+    dev: false
+
   registry.npmmirror.com/tslib/2.3.1:
     resolution:
       {
diff --git a/src/components/CountTo/index.ts b/src/components/CountTo/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/CountTo/src/CountTo.vue b/src/components/CountTo/src/CountTo.vue
new file mode 100644
index 0000000..a5d54dc
--- /dev/null
+++ b/src/components/CountTo/src/CountTo.vue
@@ -0,0 +1,175 @@
+<script setup lang="ts">
+import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
+import { isNumber } from '@/utils/is'
+import { propTypes } from '@/utils/propTypes'
+
+const props = defineProps({
+  startVal: propTypes.number.def(0),
+  endVal: propTypes.number.def(2021),
+  duration: propTypes.number.def(3000),
+  autoplay: propTypes.bool.def(false),
+  decimals: propTypes.number.validate((value: number) => value >= 0).def(0),
+  decimal: propTypes.string.def('.'),
+  separator: propTypes.string.def(','),
+  prefix: propTypes.string.def(''),
+  suffix: propTypes.string.def(''),
+  useEasing: propTypes.bool.def(true),
+  easingFn: {
+    type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
+    default(t: number, b: number, c: number, d: number) {
+      return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
+    }
+  }
+})
+
+const emit = defineEmits(['mounted', 'callback'])
+
+const formatNumber = (num: number | string) => {
+  const { decimals, decimal, separator, suffix, prefix } = props
+  num = Number(num).toFixed(decimals)
+  num += ''
+  const x = num.split('.')
+  let x1 = x[0]
+  const x2 = x.length > 1 ? decimal + x[1] : ''
+  const rgx = /(\d+)(\d{3})/
+  if (separator && !isNumber(separator)) {
+    while (rgx.test(x1)) {
+      x1 = x1.replace(rgx, '$1' + separator + '$2')
+    }
+  }
+  return prefix + x1 + x2 + suffix
+}
+
+const state = reactive<{
+  localStartVal: number
+  printVal: number | null
+  displayValue: string
+  paused: boolean
+  localDuration: number | null
+  startTime: number | null
+  timestamp: number | null
+  rAF: any
+  remaining: number | null
+}>({
+  localStartVal: props.startVal,
+  displayValue: formatNumber(props.startVal),
+  printVal: null,
+  paused: false,
+  localDuration: props.duration,
+  startTime: null,
+  timestamp: null,
+  remaining: null,
+  rAF: null
+})
+
+const displayValue = toRef(state, 'displayValue')
+
+onMounted(() => {
+  if (props.autoplay) {
+    start()
+  }
+  emit('mounted')
+})
+
+const getCountDown = computed(() => {
+  return props.startVal > props.endVal
+})
+
+watch([() => props.startVal, () => props.endVal], () => {
+  if (props.autoplay) {
+    start()
+  }
+})
+
+const start = () => {
+  const { startVal, duration } = props
+  state.localStartVal = startVal
+  state.startTime = null
+  state.localDuration = duration
+  state.paused = false
+  state.rAF = requestAnimationFrame(count)
+}
+
+const pauseResume = () => {
+  if (state.paused) {
+    resume()
+    state.paused = false
+  } else {
+    pause()
+    state.paused = true
+  }
+}
+
+const pause = () => {
+  cancelAnimationFrame(state.rAF)
+}
+
+const resume = () => {
+  state.startTime = null
+  state.localDuration = +(state.remaining as number)
+  state.localStartVal = +(state.printVal as number)
+  requestAnimationFrame(count)
+}
+
+const reset = () => {
+  state.startTime = null
+  cancelAnimationFrame(state.rAF)
+  state.displayValue = formatNumber(props.startVal)
+}
+
+const count = (timestamp: number) => {
+  const { useEasing, easingFn, endVal } = props
+  if (!state.startTime) state.startTime = timestamp
+  state.timestamp = timestamp
+  const progress = timestamp - state.startTime
+  state.remaining = (state.localDuration as number) - progress
+  if (useEasing) {
+    if (unref(getCountDown)) {
+      state.printVal =
+        state.localStartVal -
+        easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
+    } else {
+      state.printVal = easingFn(
+        progress,
+        state.localStartVal,
+        endVal - state.localStartVal,
+        state.localDuration as number
+      )
+    }
+  } else {
+    if (unref(getCountDown)) {
+      state.printVal =
+        state.localStartVal -
+        (state.localStartVal - endVal) * (progress / (state.localDuration as number))
+    } else {
+      state.printVal =
+        state.localStartVal +
+        (endVal - state.localStartVal) * (progress / (state.localDuration as number))
+    }
+  }
+  if (unref(getCountDown)) {
+    state.printVal = state.printVal < endVal ? endVal : state.printVal
+  } else {
+    state.printVal = state.printVal > endVal ? endVal : state.printVal
+  }
+  state.displayValue = formatNumber(state.printVal)
+  if (progress < (state.localDuration as number)) {
+    state.rAF = requestAnimationFrame(count)
+  } else {
+    emit('callback')
+  }
+}
+
+defineExpose({
+  pauseResume,
+  reset,
+  start,
+  pause
+})
+</script>
+
+<template>
+  <span>
+    {{ displayValue }}
+  </span>
+</template>
diff --git a/src/components/Echart/index.ts b/src/components/Echart/index.ts
new file mode 100644
index 0000000..4822092
--- /dev/null
+++ b/src/components/Echart/index.ts
@@ -0,0 +1,3 @@
+import Echart from './src/Echart.vue'
+
+export { Echart }
diff --git a/src/components/Echart/src/Echart.vue b/src/components/Echart/src/Echart.vue
new file mode 100644
index 0000000..98d9456
--- /dev/null
+++ b/src/components/Echart/src/Echart.vue
@@ -0,0 +1,110 @@
+<script setup lang="ts">
+import type { EChartsOption } from 'echarts'
+import echarts from '@/plugins/echarts'
+import { debounce } from 'lodash-es'
+import 'echarts-wordcloud'
+import { propTypes } from '@/utils/propTypes'
+import { computed, PropType, ref, unref, watch, onMounted, onBeforeUnmount, onActivated } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { isString } from '@/utils/is'
+
+const appStore = useAppStore()
+
+const props = defineProps({
+  options: {
+    type: Object as PropType<EChartsOption>,
+    required: true
+  },
+  width: propTypes.oneOfType([Number, String]).def(''),
+  height: propTypes.oneOfType([Number, String]).def('500px')
+})
+
+const isDark = computed(() => appStore.getIsDark)
+
+const theme = computed(() => {
+  const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
+
+  return echartTheme
+})
+
+const options = computed(() => {
+  return Object.assign(props.options, {
+    darkMode: unref(theme)
+  })
+})
+
+const elRef = ref<ElRef>()
+
+const echartRef = ref<echarts.ECharts>()
+
+const contentEl = ref<Element>()
+
+const styles = computed(() => {
+  const width = isString(props.width) ? props.width : `${props.width}px`
+  const height = isString(props.height) ? props.height : `${props.height}px`
+
+  return {
+    width,
+    height
+  }
+})
+
+const initChart = () => {
+  if (unref(elRef) && props.options) {
+    echartRef.value = echarts.init(unref(elRef) as HTMLElement, unref(options))
+  }
+}
+
+watch(
+  () => options.value,
+  (options) => {
+    const chart = unref(echartRef)
+    if (chart) {
+      chart?.setOption(options)
+    }
+  },
+  {
+    deep: true
+  }
+)
+
+const resizeHandler = debounce(() => {
+  const chart = unref(echartRef)
+  if (chart) {
+    chart.resize()
+  }
+}, 100)
+
+const contentResizeHandler = (e: TransitionEvent) => {
+  if (e.propertyName === 'width') {
+    resizeHandler()
+  }
+}
+
+onMounted(() => {
+  initChart()
+
+  window.addEventListener('resize', resizeHandler)
+
+  contentEl.value = document.getElementsByClassName('v-content')[0]
+  unref(contentEl) &&
+    (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeHandler)
+  unref(contentEl) &&
+    (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
+})
+
+onActivated(() => {
+  const chart = unref(echartRef)
+  if (chart) {
+    chart.resize()
+  }
+})
+</script>
+
+<template>
+  <div ref="elRef" :class="$attrs.class" :style="styles" />
+</template>
diff --git a/src/components/TagsView/src/TagsView.vue b/src/components/TagsView/src/TagsView.vue
index f06590c..986404f 100644
--- a/src/components/TagsView/src/TagsView.vue
+++ b/src/components/TagsView/src/TagsView.vue
@@ -147,6 +147,7 @@ watch(
               {
                 icon: 'ant-design:close-outlined',
                 label: t('common.closeTab'),
+                disabled: !!visitedViews?.length && selectedTag?.meta.affix,
                 command: () => {
                   closeSelectedTag(item)
                 }
@@ -243,7 +244,8 @@ watch(
         },
         {
           icon: 'ant-design:close-outlined',
-          label: t('common.closeTab')
+          label: t('common.closeTab'),
+          disabled: !!visitedViews?.length && selectedTag?.meta.affix
         },
         {
           divided: true,
diff --git a/src/layout/components/useRenderLayout.tsx b/src/layout/components/useRenderLayout.tsx
index 3b5fa19..94f3ba0 100644
--- a/src/layout/components/useRenderLayout.tsx
+++ b/src/layout/components/useRenderLayout.tsx
@@ -49,7 +49,7 @@ export const useRenderLayout = () => {
         </div>
         <div
           class={[
-            'v-app-right',
+            'v-content',
             'absolute top-0 h-[100%]',
             {
               'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
@@ -64,7 +64,6 @@ export const useRenderLayout = () => {
           <ElScrollbar
             v-loading={pageLoading.value}
             class={[
-              'v-content',
               {
                 '!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
                   fixedHeader.value
@@ -110,7 +109,7 @@ export const useRenderLayout = () => {
           <Menu class="!h-full"></Menu>
           <div
             class={[
-              'v-app-right',
+              'v-content',
               'h-[100%]',
               {
                 'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
@@ -124,7 +123,6 @@ export const useRenderLayout = () => {
             <ElScrollbar
               v-loading={pageLoading.value}
               class={[
-                'v-content',
                 {
                   '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
                     fixedHeader.value && tagsView.value
@@ -163,11 +161,10 @@ export const useRenderLayout = () => {
           <Menu class="flex-1 px-10px h-[var(--top-tool-height)]"></Menu>
           <ToolHeader></ToolHeader>
         </div>
-        <div class="v-app-right h-full w-full">
+        <div class="v-content h-full w-full">
           <ElScrollbar
             v-loading={pageLoading.value}
             class={[
-              'v-content',
               {
                 'mt-[var(--tags-view-height)]': fixedHeader.value
               }
@@ -205,7 +202,7 @@ export const useRenderLayout = () => {
           {/* <Menu class="!h-full"></Menu> */}
           <div
             class={[
-              'v-app-right',
+              'v-content',
               'h-[100%]',
               {
                 'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
@@ -219,7 +216,6 @@ export const useRenderLayout = () => {
             <ElScrollbar
               v-loading={pageLoading.value}
               class={[
-                'v-content',
                 {
                   '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
                     fixedHeader.value && tagsView.value
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 17e551d..be4af66 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -69,7 +69,9 @@ export default {
     menu11: 'Menu1-1',
     menu111: 'Menu1-1-1',
     menu12: 'Menu1-2',
-    menu2: 'Menu2'
+    menu2: 'Menu2',
+    dashboard: 'Dashboard',
+    analysis: 'Analysis'
   },
   formDemo: {
     input: 'Input',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index f18940f..0e50ba0 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -69,7 +69,9 @@ export default {
     menu11: '菜单1-1',
     menu111: '菜单1-1-1',
     menu12: '菜单1-2',
-    menu2: '菜单2'
+    menu2: '菜单2',
+    dashboard: '首页',
+    analysis: '分析页'
   },
   formDemo: {
     input: '输入框',
diff --git a/src/permission.ts b/src/permission.ts
index 7d1b18a..f9ef5d3 100644
--- a/src/permission.ts
+++ b/src/permission.ts
@@ -38,7 +38,7 @@ router.beforeEach(async (to, from, next) => {
       const redirect = decodeURIComponent(redirectPath as string)
       const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
       permissionStore.setIsAddRouters(true)
-      next(nextData)
+      next(to.path === '/' ? { path: permissionStore.addRouters[0]?.path as string } : nextData)
     }
   } else {
     if (whiteList.indexOf(to.path) !== -1) {
diff --git a/src/plugins/echarts/index.ts b/src/plugins/echarts/index.ts
new file mode 100644
index 0000000..54bc668
--- /dev/null
+++ b/src/plugins/echarts/index.ts
@@ -0,0 +1,33 @@
+import * as echarts from 'echarts/core'
+
+import { BarChart, LineChart, PieChart, MapChart, PictorialBarChart } from 'echarts/charts'
+
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  PolarComponent,
+  AriaComponent,
+  ParallelComponent,
+  LegendComponent
+} from 'echarts/components'
+
+import { CanvasRenderer } from 'echarts/renderers'
+
+echarts.use([
+  LegendComponent,
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  PolarComponent,
+  AriaComponent,
+  ParallelComponent,
+  BarChart,
+  LineChart,
+  PieChart,
+  MapChart,
+  CanvasRenderer,
+  PictorialBarChart
+])
+
+export default echarts
diff --git a/src/router/index.ts b/src/router/index.ts
index 2b6be16..4e371b2 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -37,6 +37,29 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
 ]
 
 export const asyncRouterMap: AppRouteRecordRaw[] = [
+  {
+    path: '/dashboard',
+    component: Layout,
+    redirect: '/dashboard/analysis',
+    name: 'Dashboard',
+    meta: {
+      title: t('router.dashboard'),
+      icon: 'ant-design:dashboard-filled',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'analysis',
+        component: () => import('@/views/Dashboard/Analysis.vue'),
+        name: 'Analysis',
+        meta: {
+          title: t('router.analysis'),
+          noCache: true,
+          affix: true
+        }
+      }
+    ]
+  },
   {
     path: '/level',
     component: Layout,
diff --git a/src/views/Dashboard/Analysis.vue b/src/views/Dashboard/Analysis.vue
new file mode 100644
index 0000000..4dde1e6
--- /dev/null
+++ b/src/views/Dashboard/Analysis.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+// import { ElRow, ElCol } from 'element-plus'
+</script>
+
+<template>
+  <div>dddd</div>
+</template>
diff --git a/src/views/Dashboard/echarts-data.ts b/src/views/Dashboard/echarts-data.ts
new file mode 100644
index 0000000..e69de29