瀏覽代碼

wip: vite版重构中

kailong321200875 3 年之前
父節點
當前提交
0f5c55c36d

+ 1 - 0
.eslintignore

@@ -5,3 +5,4 @@
 /*.d.ts
 /test/unit/coverage/
 /node_modules/*
+/src/env.d.ts

+ 1 - 0
.stylelintignore

@@ -2,3 +2,4 @@
 /public/*
 public/*
 /dist*
+/src/env.d.ts

+ 6 - 0
components.d.ts

@@ -5,9 +5,13 @@
 declare module 'vue' {
   export interface GlobalComponents {
     404: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/Error/404.vue')['default']
+    CountTo: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/CountTo/index.vue')['default']
+    Echart: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/Echart/index.vue')['default']
+    ElAlert: typeof import('element-plus/es')['ElAlert']
     ElBacktop: typeof import('element-plus/es')['ElBacktop']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
+    ElCol: typeof import('element-plus/es')['ElCol']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -17,6 +21,7 @@ declare module 'vue' {
     ElInput: typeof import('element-plus/es')['ElInput']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
@@ -25,6 +30,7 @@ declare module 'vue' {
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     HelloWorld: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/HelloWorld.vue')['default']
     ParentView: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/ParentView/index.vue')['default']
+    Preview: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/Preview/index.vue')['default']
     Redirect: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/Redirect/index.vue')['default']
     SvgIcon: typeof import('C:/Users/Saber/Documents/HBuilderProjects/vue-element-plus-admin/src/components/SvgIcon/index.vue')['default']
   }

+ 3 - 0
package.json

@@ -26,7 +26,10 @@
     "@element-plus/icons": "^0.0.11",
     "@vueuse/core": "^6.5.3",
     "axios": "^0.22.0",
+    "echarts": "^5.2.1",
+    "echarts-wordcloud": "^2.0.0",
     "element-plus": "1.1.0-beta.20",
+    "intro.js": "^4.2.2",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
     "nprogress": "^0.2.0",

+ 1 - 0
src/assets/icons/resume.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307154239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7317" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="7318"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="7319"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="7320"></path></svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/rotate.svg


+ 1 - 0
src/assets/icons/scale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307195033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8116" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M887.081 904.791a25.8 25.8 0 0 1-18.376-7.619L705.618 734.075l-4.163 3.369c-58.255 47.18-131.522 73.16-206.32 73.16-181.07 0-328.377-147.308-328.377-328.367 0-181.068 147.308-328.376 328.377-328.376 181.063 0 328.376 147.308 328.376 328.376 0 77.072-27.412 152.07-77.169 211.17l-3.522 4.173 162.719 162.744a25.846 25.846 0 0 1 7.639 18.432 26.081 26.081 0 0 1-26.051 26.045l-0.046-0.01zM495.13 205.957c-152.336 0-276.27 123.935-276.27 276.27 0 152.33 123.934 276.27 276.27 276.27 152.34 0 276.275-123.94 276.275-276.27 0-152.335-123.935-276.27-276.275-276.27z" p-id="8117"></path><path d="M626.545 508.355h-262.83a26.127 26.127 0 0 1 0-52.255h262.83a26.127 26.127 0 0 1 0 52.255z" p-id="8118"></path><path d="M495.13 639.77a26.127 26.127 0 0 1-26.128-26.128v-262.83a26.127 26.127 0 0 1 52.255 0v262.835a26.127 26.127 0 0 1-26.127 26.123z" p-id="8119"></path></svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/unrotate.svg


+ 1 - 0
src/assets/icons/unscale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595308005241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M750.3 198.7C598 46.4 351.1 46.4 198.7 198.7s-152.3 399.2 0 551.5C345.1 896.6 578.8 902.3 732 767.3l172.1 172.1 35.4-35.4-172.1-171.9c135-153.2 129.3-387-17.1-533.4z m39.3 403.8c-17.1 42.1-42.2 80-74.7 112.4-32.5 32.5-70.3 57.6-112.4 74.7-40.7 16.5-83.8 24.9-128 24.9s-87.2-8.4-128-24.9c-42.1-17.1-80-42.2-112.4-74.7s-57.6-70.3-74.7-112.4c-16.5-40.7-24.9-83.8-24.9-128s8.4-87.2 24.9-128c17.1-42.1 42.2-80 74.7-112.4s70.3-57.6 112.4-74.7c40.7-16.5 83.8-24.9 128-24.9s87.2 8.4 128 24.9c42.1 17.1 80 42.2 112.4 74.7 32.5 32.5 57.6 70.3 74.7 112.4 16.5 40.7 24.9 83.8 24.9 128s-8.4 87.3-24.9 128zM671 502H271v-50h400v50z" p-id="9879"></path></svg>

+ 155 - 0
src/components/CountTo/index.vue

@@ -0,0 +1,155 @@
+<template>
+  <span>
+    {{ displayValue }}
+  </span>
+</template>
+<script setup lang="ts" name="CountTo">
+import { reactive, computed, watch, onMounted, unref, toRef } from 'vue'
+import { countToProps } from './props'
+import { isNumber } from '@/utils/validate'
+import { requestAnimationFrame, cancelAnimationFrame } from '@/utils/animation'
+
+const props = defineProps(countToProps)
+
+const emit = defineEmits(['mounted', 'callback'])
+
+defineExpose({
+  pauseResume,
+  reset
+})
+
+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()
+  }
+})
+
+function start() {
+  const { startVal, duration } = props
+  state.localStartVal = startVal
+  state.startTime = null
+  state.localDuration = duration
+  state.paused = false
+  state.rAF = requestAnimationFrame(count)
+}
+
+function pauseResume() {
+  if (state.paused) {
+    resume()
+    state.paused = false
+  } else {
+    pause()
+    state.paused = true
+  }
+}
+
+function pause() {
+  cancelAnimationFrame(state.rAF)
+}
+
+function resume() {
+  state.startTime = null
+  state.localDuration = +(state.remaining as number)
+  state.localStartVal = +(state.printVal as number)
+  requestAnimationFrame(count)
+}
+
+function reset() {
+  state.startTime = null
+  cancelAnimationFrame(state.rAF)
+  state.displayValue = formatNumber(props.startVal)
+}
+
+function 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')
+  }
+}
+
+function 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
+}
+</script>

+ 62 - 0
src/components/CountTo/props.ts

@@ -0,0 +1,62 @@
+import { PropType } from 'vue'
+export const countToProps = {
+  startVal: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 0
+  },
+  endVal: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 2017
+  },
+  duration: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 3000
+  },
+  autoplay: {
+    type: Boolean as PropType<boolean>,
+    required: false,
+    default: true
+  },
+  decimals: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 0,
+    validator(value: number) {
+      return value >= 0
+    }
+  },
+  decimal: {
+    type: String as PropType<string>,
+    required: false,
+    default: '.'
+  },
+  separator: {
+    type: String as PropType<string>,
+    required: false,
+    default: ','
+  },
+  prefix: {
+    type: String as PropType<string>,
+    required: false,
+    default: ''
+  },
+  suffix: {
+    type: String as PropType<string>,
+    required: false,
+    default: ''
+  },
+  useEasing: {
+    type: Boolean as PropType<boolean>,
+    required: false,
+    default: 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
+    }
+  }
+}

+ 91 - 0
src/components/Echart/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <div ref="echartRef" :class="className" :style="{ height: height, width: width }"></div>
+</template>
+
+<script setup lang="ts" name="Echart">
+import { PropType, onMounted, watch, computed, onBeforeUnmount, onActivated, ref, unref } from 'vue'
+import type { EChartsOption } from 'echarts'
+import echarts from '@/plugins/echarts'
+import { debounce } from 'lodash-es'
+import 'echarts-wordcloud'
+
+type ThemeType = 'light' | 'dark' | 'default'
+
+const props = defineProps({
+  options: {
+    type: Object as PropType<EChartsOption>,
+    required: true
+  },
+  className: {
+    type: String as PropType<string>,
+    default: ''
+  },
+  height: {
+    type: String as PropType<string>,
+    default: '500px'
+  },
+  width: {
+    type: String as PropType<string>,
+    default: ''
+  },
+  theme: {
+    type: String as PropType<ThemeType>,
+    default: 'default'
+  }
+})
+
+let chartRef: Nullable<echarts.ECharts> = null
+let sidebarElm: Nullable<Element | any> = null
+let __resizeHandler: Nullable<any> = null
+const echartOptions = computed(() => props.options)
+const echartRef = ref<Nullable<HTMLElement>>(null)
+
+watch(
+  echartOptions,
+  (options: EChartsOption) => {
+    ;(chartRef as echarts.ECharts).setOption(options)
+  },
+  {
+    deep: true
+  }
+)
+
+function initChart() {
+  chartRef = echarts.init(unref(echartRef) as HTMLElement, props.theme)
+  chartRef.setOption(props.options)
+}
+
+function sidebarResizeHandler(e: any): void {
+  if (e.propertyName === 'width') {
+    if (__resizeHandler) {
+      __resizeHandler()
+    }
+  }
+}
+
+onMounted(() => {
+  initChart()
+
+  __resizeHandler = debounce(() => {
+    if (chartRef) {
+      chartRef.resize()
+    }
+  }, 100)
+  window.addEventListener('resize', __resizeHandler)
+  sidebarElm = document.getElementsByClassName('sidebar__wrap')[0]
+  sidebarElm && sidebarElm.addEventListener('transitionend', sidebarResizeHandler)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', __resizeHandler)
+
+  sidebarElm && sidebarElm.removeEventListener('transitionend', sidebarResizeHandler)
+})
+
+onActivated(() => {
+  // 防止keep-alive之后图表变形
+  if (chartRef) {
+    chartRef.resize()
+  }
+})
+</script>

+ 33 - 0
src/components/Preview/index.ts

@@ -0,0 +1,33 @@
+import ImgPreview from './index.vue'
+import { isClient } from '@/utils/validate'
+
+import type { Options, Props } from './types'
+
+import { createVNode, render } from 'vue'
+
+let instance: any = null
+
+export function createImgPreview(options: Options) {
+  if (!isClient) return
+  const {
+    imageList,
+    show = true,
+    index = 0,
+    onSelect = null,
+    onClose = null,
+    zIndex = 500
+  } = options
+
+  const propsData: Partial<Props> = {}
+  const container = document.createElement('div')
+  propsData.imageList = imageList
+  propsData.show = show
+  propsData.index = index
+  propsData.zIndex = zIndex
+  propsData.onSelect = onSelect
+  propsData.onClose = onClose
+
+  document.body.appendChild(container)
+  instance = createVNode(ImgPreview, propsData)
+  render(instance, container)
+}

+ 429 - 0
src/components/Preview/index.vue

@@ -0,0 +1,429 @@
+<template>
+  <transition name="viewer-fade">
+    <div
+      v-show="show"
+      ref="wrapElRef"
+      tabindex="-1"
+      :style="{ 'z-index': zIndex }"
+      class="image-viewer__wrapper"
+    >
+      <div class="image-viewer__mask"></div>
+      <!-- CLOSE -->
+      <span class="image-viewer__btn image-viewer__close" @click="hide">
+        <i class="el-icon-circle-close iconfont"></i>
+      </span>
+      <!-- ARROW -->
+      <template v-if="!isSingle">
+        <span
+          class="image-viewer__btn image-viewer__prev"
+          :class="{ 'is-disabled': !infinite && isFirst }"
+          @click="prev"
+        >
+          <i class="el-icon-arrow-left iconfont"></i>
+        </span>
+        <span
+          class="image-viewer__btn image-viewer__next"
+          :class="{ 'is-disabled': !infinite && isLast }"
+          @click="next"
+        >
+          <i class="el-icon-arrow-right iconfont"></i>
+        </span>
+      </template>
+      <!-- ACTIONS -->
+      <div class="image-viewer__btn image-viewer__actions">
+        <div class="image-viewer__actions__inner">
+          <svg-icon class="iconfont" icon-class="unscale" @click="handleActions('zoomOut')" />
+          <svg-icon class="iconfont" icon-class="scale" @click="handleActions('zoomIn')" />
+          <svg-icon class="iconfont" icon-class="resume" @click="toggleMode" />
+          <svg-icon
+            class="iconfont"
+            icon-class="unrotate"
+            @click="handleActions('anticlocelise')"
+          />
+          <svg-icon class="iconfont" icon-class="rotate" @click="handleActions('clocelise')" />
+        </div>
+      </div>
+      <!-- CANVAS -->
+      <div class="image-viewer__canvas">
+        <img
+          ref="imgRef"
+          :src="currentImg"
+          :style="imgStyle"
+          class="image-viewer__img"
+          @load="handleImgLoad"
+          @error="handleImgError"
+          @mousedown="handleMouseDown"
+          @click="select"
+        />
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script setup lang="ts" name="Preview">
+import { ref, reactive, computed, watch, nextTick, unref } from 'vue'
+import { previewProps } from './props'
+import { isFirefox } from '@/utils/validate'
+import { on, off } from '@/utils/dom-utils'
+import throttle from 'lodash-es/throttle'
+import SvgIcon from '_c/SvgIcon/index.vue'
+const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
+
+const props = defineProps(previewProps)
+
+const infinite = ref<boolean>(true)
+const loading = ref<boolean>(false)
+const show = ref<boolean>(props.show)
+const index = ref<number>(props.index)
+const transform = reactive({
+  scale: 1,
+  deg: 0,
+  offsetX: 0,
+  offsetY: 0,
+  enableTransition: false
+})
+const isSingle = computed((): boolean => props.imageList.length <= 1)
+const isFirst = computed((): boolean => index.value === 0)
+const isLast = computed((): boolean => index.value === props.imageList.length - 1)
+const currentImg = computed((): string => props.imageList[index.value])
+const imgStyle = computed(() => {
+  const { scale, deg, offsetX, offsetY, enableTransition } = transform
+  const style = {
+    transform: `scale(${scale}) rotate(${deg}deg)`,
+    transition: enableTransition ? 'transform .3s' : '',
+    'margin-left': `${offsetX}px`,
+    'margin-top': `${offsetY}px`
+  }
+  return style
+})
+
+const wrapElRef = ref<HTMLElement | null>(null)
+const imgRef = ref<HTMLElement | null>(null)
+
+let _keyDownHandler: Function | null = null
+let _mouseWheelHandler: Function | null = null
+let _dragHandler: Function | null = null
+
+watch(
+  () => index.value,
+  () => {
+    reset()
+  }
+)
+
+watch(
+  () => currentImg.value,
+  () => {
+    nextTick(() => {
+      const $img = unref(imgRef) as any
+      if (!$img.complete) {
+        loading.value = true
+      }
+    })
+  }
+)
+
+watch(
+  () => show.value,
+  (show: boolean) => {
+    if (show) {
+      nextTick(() => {
+        ;(unref(wrapElRef) as any).focus()
+        document.body.style.overflow = 'hidden'
+        deviceSupportInstall()
+      })
+    } else {
+      nextTick(() => {
+        document.body.style.overflow = 'auto'
+        deviceSupportUninstall()
+      })
+    }
+  },
+  {
+    immediate: true
+  }
+)
+
+function hide(): void {
+  show.value = false
+  if (typeof props.onClose === 'function') {
+    props.onClose(index.value)
+  }
+}
+
+function select(): void {
+  if (typeof props.onSelect === 'function') {
+    props.onSelect(index.value)
+  }
+}
+
+function deviceSupportInstall(): void {
+  _keyDownHandler = throttle((e: any) => {
+    const keyCode = e.keyCode
+    switch (keyCode) {
+      // ESC
+      case 27:
+        hide()
+        break
+      // SPACE
+      case 32:
+        toggleMode()
+        break
+      // LEFT_ARROW
+      case 37:
+        prev()
+        break
+      // UP_ARROW
+      case 38:
+        handleActions('zoomIn')
+        break
+      // RIGHT_ARROW
+      case 39:
+        next()
+        break
+      // DOWN_ARROW
+      case 40:
+        handleActions('zoomOut')
+        break
+    }
+  })
+
+  _mouseWheelHandler = throttle((e: any) => {
+    const delta = e.wheelDelta ? e.wheelDelta : -e.detail
+    if (delta > 0) {
+      handleActions('zoomIn', {
+        zoomRate: 0.015,
+        enableTransition: false
+      })
+    } else {
+      handleActions('zoomOut', {
+        zoomRate: 0.015,
+        enableTransition: false
+      })
+    }
+  })
+  on(document, 'keydown', _keyDownHandler as any)
+  on(document, mousewheelEventName, _mouseWheelHandler as any)
+}
+
+function deviceSupportUninstall(): void {
+  off(document, 'keydown', _keyDownHandler)
+  off(document, mousewheelEventName, _mouseWheelHandler)
+  _keyDownHandler = null
+  _mouseWheelHandler = null
+}
+
+function handleImgLoad(): void {
+  loading.value = false
+}
+
+function handleImgError(e: any): void {
+  loading.value = false
+  e.target.alt = '加载失败'
+}
+
+function handleMouseDown(e: any): void {
+  if (loading.value || e.button !== 0) return
+  const { offsetX, offsetY } = transform
+  const startX = e.pageX
+  const startY = e.pageY
+  _dragHandler = throttle((ev: any) => {
+    transform.offsetX = offsetX + ev.pageX - startX
+    transform.offsetY = offsetY + ev.pageY - startY
+  })
+  on(document, 'mousemove', _dragHandler as any)
+  on(document, 'mouseup', () => {
+    off(document, 'mousemove', _dragHandler as any)
+  })
+
+  e.preventDefault()
+}
+
+function reset(): void {
+  transform.scale = 1
+  transform.deg = 0
+  transform.offsetX = 0
+  transform.offsetY = 0
+  transform.enableTransition = false
+}
+
+function toggleMode(): void {
+  if (loading.value) return
+  reset()
+}
+
+function prev(): void {
+  if (isFirst.value && !infinite.value) return
+  const len = props.imageList.length
+  index.value = (index.value - 1 + len) % len
+}
+
+function next(): void {
+  if (isLast.value && !infinite.value) return
+  const len = props.imageList.length
+  index.value = (index.value + 1) % len
+}
+
+function handleActions(action: string, options: any = {}): void {
+  if (loading.value) return
+  const style = {
+    zoomRate: 0.2,
+    rotateDeg: 90,
+    enableTransition: true,
+    ...options
+  }
+  const { zoomRate, rotateDeg, enableTransition } = style
+  switch (action) {
+    case 'zoomOut':
+      if (transform.scale > 0.2) {
+        transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3))
+      }
+      break
+    case 'zoomIn':
+      transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3))
+      break
+    case 'clocelise':
+      transform.deg += rotateDeg
+      break
+    case 'anticlocelise':
+      transform.deg -= rotateDeg
+      break
+  }
+  transform.enableTransition = enableTransition
+}
+</script>
+
+<style lang="less" scoped>
+.iconfont {
+  cursor: pointer;
+}
+
+.image-viewer__wrapper {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.image-viewer__btn {
+  position: absolute;
+  z-index: 1;
+  display: flex;
+  cursor: pointer;
+  border-radius: 50%;
+  opacity: 0.8;
+  box-sizing: border-box;
+  user-select: none;
+  align-items: center;
+  justify-content: center;
+}
+
+.image-viewer__close {
+  top: 40px;
+  right: 40px;
+  width: 40px;
+  height: 40px;
+  font-size: 40px;
+  color: #fff;
+}
+
+.image-viewer__canvas {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  justify-content: center;
+  align-items: center;
+}
+
+.image-viewer__actions {
+  bottom: 30px;
+  left: 50%;
+  width: 282px;
+  height: 44px;
+  padding: 0 23px;
+  background-color: #606266;
+  border-color: #fff;
+  border-radius: 22px;
+  transform: translateX(-50%);
+
+  .image-viewer__actions__inner {
+    display: flex;
+    width: 100%;
+    height: 100%;
+    font-size: 23px;
+    color: #fff;
+    text-align: justify;
+    cursor: default;
+    align-items: center;
+    justify-content: space-around;
+  }
+}
+
+.image-viewer__prev {
+  top: 50%;
+  left: 40px;
+  width: 44px;
+  height: 44px;
+  font-size: 24px;
+  color: #fff;
+  background-color: #606266;
+  border-color: #fff;
+  transform: translateY(-50%);
+}
+
+.image-viewer__next {
+  top: 50%;
+  right: 40px;
+  width: 44px;
+  height: 44px;
+  font-size: 24px;
+  color: #fff;
+  text-indent: 2px;
+  background-color: #606266;
+  border-color: #fff;
+  transform: translateY(-50%);
+}
+
+.image-viewer__mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  opacity: 0.5;
+}
+
+.viewer-fade-enter-active {
+  animation: viewer-fade-in 0.3s;
+}
+
+.viewer-fade-leave-active {
+  animation: viewer-fade-out 0.3s;
+}
+
+@keyframes viewer-fade-in {
+  0% {
+    opacity: 0;
+    transform: translate3d(0, -20px, 0);
+  }
+
+  100% {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes viewer-fade-out {
+  0% {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+
+  100% {
+    opacity: 0;
+    transform: translate3d(0, -20px, 0);
+  }
+}
+</style>

+ 28 - 0
src/components/Preview/props.ts

@@ -0,0 +1,28 @@
+import { PropType } from 'vue'
+
+export const previewProps = {
+  index: {
+    type: Number as PropType<number>,
+    default: 0
+  },
+  zIndex: {
+    type: Number as PropType<number>,
+    default: 100
+  },
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  },
+  imageList: {
+    type: [Array] as PropType<string[]>,
+    default: []
+  },
+  onClose: {
+    type: Function as PropType<Function>,
+    default: null
+  },
+  onSelect: {
+    type: Function as PropType<Function>,
+    default: null
+  }
+}

+ 18 - 0
src/components/Preview/types.ts

@@ -0,0 +1,18 @@
+export interface Options {
+  show?: boolean
+  imageList: string[]
+  index?: number
+  zIndex?: number
+  onSelect?: Function | null
+  onClose?: Function | null
+}
+
+export interface Props {
+  show: boolean
+  instance: Props
+  imageList: string[]
+  index: number
+  zIndex: number
+  onSelect: Function | null
+  onClose: Function | null
+}

+ 1 - 1
src/env.d.ts

@@ -1,4 +1,4 @@
-// / <reference types="vite/client" />
+/// <reference types="vite/client" />
 
 declare module '*.vue' {
   import { DefineComponent } from 'vue'

+ 5 - 4
src/layout/components/Sider/SiderItem.vue

@@ -36,7 +36,7 @@
       <template #title>
         <item v-if="siderItem.meta" :icon="siderItem?.meta?.icon" :title="siderItem.meta.title" />
       </template>
-      <sider-item
+      <sider-item-com
         v-for="child in siderItem.children"
         :key="child.path"
         :is-nest="true"
@@ -51,7 +51,6 @@
 <script setup lang="ts" name="SiderItemCom">
 import { PropType, ref, computed } from 'vue'
 import type { RouteRecordRaw } from 'vue-router'
-import path from 'path-browserify'
 import { isExternal } from '@/utils/validate'
 import Item from './Item.vue'
 import { usePermissionStore } from '@/store/modules/permission'
@@ -78,7 +77,6 @@ const props = defineProps({
     default: 'Classic'
   }
 })
-
 const onlyOneChild = ref<any>(null)
 
 const activeTab = computed(() => permissionStore.getActiveTab)
@@ -114,7 +112,10 @@ function resolvePath(routePath: string, otherPath?: string): string {
   if (isExternal(routePath)) {
     return routePath
   }
-  return path.resolve(otherPath || props.basePath, routePath)
+  return (
+    ((otherPath || props.basePath) === '/' ? '' : otherPath || props.basePath) +
+    (routePath ? '/' + routePath : '')
+  )
 }
 </script>
 

+ 1 - 3
src/layout/components/Sider/index.vue

@@ -48,9 +48,7 @@ defineProps({
 })
 
 const { currentRoute, push } = useRouter()
-const routers = computed(() => {
-  return permissionStore.getRouters
-})
+const routers = computed(() => permissionStore.getRouters)
 const activeMenu = computed(() => {
   const { meta, path } = currentRoute.value
   // if set path, the sidebar will highlight the path you set

+ 33 - 0
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

+ 215 - 213
src/router/index.ts

@@ -2,11 +2,13 @@ import { createRouter, createWebHashHistory } from 'vue-router'
 import type { RouteRecordRaw } from 'vue-router'
 import { AppRouteRecordRaw } from './types'
 import type { App } from 'vue'
-// import { getParentLayout } from './utils'
+import { getParentLayout } from './utils'
 
 /* Layout */
 const Layout = () => import('../layout/index.vue')
 
+// const ParentView = () => import('_c/ParentView/index.vue')
+
 /**
 * redirect: noredirect        当设置 noredirect 的时候该路由在面包屑导航中不可被点击
 * name:'router-name'          设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
@@ -84,163 +86,163 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
         }
       }
     ]
+  },
+  {
+    path: '/external-link',
+    component: Layout,
+    meta: {},
+    children: [
+      {
+        path: 'http://8.133.179.48:4000/dist-doc/',
+        meta: { title: '文档', icon: 'documentation' }
+      }
+    ]
+  },
+  {
+    path: '/guide',
+    component: Layout,
+    name: 'Guide',
+    meta: {},
+    children: [
+      {
+        path: 'index',
+        component: () => import('_v/guide/index.vue'),
+        name: 'GuideDemo',
+        meta: {
+          title: '引导页',
+          icon: 'guide'
+        }
+      }
+    ]
   }
-  // {
-  //   path: '/external-link',
-  //   component: Layout,
-  //   meta: {},
-  //   children: [
-  //     {
-  //       path: 'http://8.133.179.48:4000/dist-doc/',
-  //       meta: { title: '文档', icon: 'documentation' }
-  //     }
-  //   ]
-  // },
-  // {
-  //   path: '/guide',
-  //   component: Layout,
-  //   name: 'Guide',
-  //   meta: {},
-  //   children: [
-  //     {
-  //       path: 'index',
-  //       component: () => import('_v/guide/index.vue'),
-  //       name: 'GuideDemo',
-  //       meta: {
-  //         title: '引导页',
-  //         icon: 'guide'
-  //       }
-  //     }
-  //   ]
-  // }
 ]
 
 export const asyncRouterMap: AppRouteRecordRaw[] = [
-  // {
-  //   path: '/components-demo',
-  //   component: Layout,
-  //   redirect: '/components-demo/echarts',
-  //   name: 'ComponentsDemo',
-  //   meta: {
-  //     title: '功能组件',
-  //     icon: 'component',
-  //     alwaysShow: true
-  //   },
-  //   children: [
-  //     {
-  //       path: 'echarts',
-  //       component: () => import('_v/components-demo/echarts/index.vue'),
-  //       name: 'EchartsDemo',
-  //       meta: {
-  //         title: '图表'
-  //       }
-  //     },
-  //     {
-  //       path: 'preview',
-  //       component: () => import('_v/components-demo/preview/index.vue'),
-  //       name: 'PreviewDemo',
-  //       meta: {
-  //         title: '图片预览'
-  //       }
-  //     },
-  //     {
-  //       path: 'button',
-  //       component: () => import('_v/components-demo/button/index.vue'),
-  //       name: 'ButtonDemo',
-  //       meta: {
-  //         title: '按钮'
-  //       }
-  //     },
-  //     {
-  //       path: 'message',
-  //       component: () => import('_v/components-demo/message/index.vue'),
-  //       name: 'MessageDemo',
-  //       meta: {
-  //         title: '消息提示'
-  //       }
-  //     },
-  //     {
-  //       path: 'count-to',
-  //       component: () => import('_v/components-demo/count-to/index.vue'),
-  //       name: 'CountToDemo',
-  //       meta: {
-  //         title: '数字动画'
-  //       }
-  //     },
-  //     {
-  //       path: 'search',
-  //       component: () => import('_v/components-demo/search/index.vue'),
-  //       name: 'SearchDemo',
-  //       meta: {
-  //         title: '查询'
-  //       }
-  //     },
-  //     {
-  //       path: 'editor',
-  //       component: () => import('_v/components-demo/editor/index.vue'),
-  //       name: 'EditorDemo',
-  //       meta: {
-  //         title: '富文本编辑器'
-  //       }
-  //     },
-  //     {
-  //       path: 'markdown',
-  //       component: () => import('_v/components-demo/markdown/index.vue'),
-  //       name: 'MarkdownDemo',
-  //       meta: {
-  //         title: 'markdown编辑器'
-  //       }
-  //     },
-  //     {
-  //       path: 'dialog',
-  //       component: () => import('_v/components-demo/dialog/index.vue'),
-  //       name: 'DialogDemo',
-  //       meta: {
-  //         title: '弹窗'
-  //       }
-  //     },
-  //     {
-  //       path: 'more',
-  //       component: () => import('_v/components-demo/more/index.vue'),
-  //       name: 'MoreDemo',
-  //       meta: {
-  //         title: '显示更多'
-  //       }
-  //     },
-  //     {
-  //       path: 'detail',
-  //       component: () => import('_v/components-demo/detail/index.vue'),
-  //       name: 'DetailDemo',
-  //       meta: {
-  //         title: '详情组件'
-  //       }
-  //     },
-  //     {
-  //       path: 'qrcode',
-  //       component: () => import('_v/components-demo/qrcode/index.vue'),
-  //       name: 'QrcodeDemo',
-  //       meta: {
-  //         title: '二维码组件'
-  //       }
-  //     },
-  //     {
-  //       path: 'avatars',
-  //       component: () => import('_v/components-demo/avatars/index.vue'),
-  //       name: 'AvatarsDemo',
-  //       meta: {
-  //         title: '头像组'
-  //       }
-  //     },
-  //     {
-  //       path: 'highlight',
-  //       component: () => import('_v/components-demo/highlight/index.vue'),
-  //       name: 'HighlightDemo',
-  //       meta: {
-  //         title: '文字高亮'
-  //       }
-  //     }
-  //   ]
-  // },
+  {
+    path: '/components-demo',
+    component: Layout,
+    redirect: '/components-demo/echarts',
+    name: 'ComponentsDemo',
+    meta: {
+      title: '功能组件',
+      icon: 'component',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'echarts',
+        component: () => import('_v/components-demo/echarts/index.vue'),
+        name: 'EchartsDemo',
+        meta: {
+          title: '图表'
+        }
+      },
+      {
+        path: 'preview',
+        component: () => import('_v/components-demo/preview/index.vue'),
+        name: 'PreviewDemo',
+        meta: {
+          title: '图片预览'
+        }
+      }
+      // {
+      //   path: 'button',
+      //   component: () => import('_v/components-demo/button/index.vue'),
+      //   name: 'ButtonDemo',
+      //   meta: {
+      //     title: '按钮'
+      //   }
+      // },
+      // {
+      //   path: 'message',
+      //   component: () => import('_v/components-demo/message/index.vue'),
+      //   name: 'MessageDemo',
+      //   meta: {
+      //     title: '消息提示'
+      //   }
+      // },
+      // {
+      //   path: 'count-to',
+      //   component: () => import('_v/components-demo/count-to/index.vue'),
+      //   name: 'CountToDemo',
+      //   meta: {
+      //     title: '数字动画'
+      //   }
+      // },
+      // {
+      //   path: 'search',
+      //   component: () => import('_v/components-demo/search/index.vue'),
+      //   name: 'SearchDemo',
+      //   meta: {
+      //     title: '查询'
+      //   }
+      // },
+      // {
+      //   path: 'editor',
+      //   component: () => import('_v/components-demo/editor/index.vue'),
+      //   name: 'EditorDemo',
+      //   meta: {
+      //     title: '富文本编辑器'
+      //   }
+      // },
+      // {
+      //   path: 'markdown',
+      //   component: () => import('_v/components-demo/markdown/index.vue'),
+      //   name: 'MarkdownDemo',
+      //   meta: {
+      //     title: 'markdown编辑器'
+      //   }
+      // },
+      // {
+      //   path: 'dialog',
+      //   component: () => import('_v/components-demo/dialog/index.vue'),
+      //   name: 'DialogDemo',
+      //   meta: {
+      //     title: '弹窗'
+      //   }
+      // },
+      // {
+      //   path: 'more',
+      //   component: () => import('_v/components-demo/more/index.vue'),
+      //   name: 'MoreDemo',
+      //   meta: {
+      //     title: '显示更多'
+      //   }
+      // },
+      // {
+      //   path: 'detail',
+      //   component: () => import('_v/components-demo/detail/index.vue'),
+      //   name: 'DetailDemo',
+      //   meta: {
+      //     title: '详情组件'
+      //   }
+      // },
+      // {
+      //   path: 'qrcode',
+      //   component: () => import('_v/components-demo/qrcode/index.vue'),
+      //   name: 'QrcodeDemo',
+      //   meta: {
+      //     title: '二维码组件'
+      //   }
+      // },
+      // {
+      //   path: 'avatars',
+      //   component: () => import('_v/components-demo/avatars/index.vue'),
+      //   name: 'AvatarsDemo',
+      //   meta: {
+      //     title: '头像组'
+      //   }
+      // },
+      // {
+      //   path: 'highlight',
+      //   component: () => import('_v/components-demo/highlight/index.vue'),
+      //   name: 'HighlightDemo',
+      //   meta: {
+      //     title: '文字高亮'
+      //   }
+      // }
+    ]
+  },
   // {
   //   path: '/table-demo',
   //   component: Layout,
@@ -481,65 +483,65 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
   //     }
   //   ]
   // },
-  // {
-  //   path: '/level',
-  //   component: Layout,
-  //   redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //   name: 'Level',
-  //   meta: {
-  //     title: '多级菜单缓存',
-  //     icon: 'nested'
-  //   },
-  //   children: [
-  //     {
-  //       path: 'menu1',
-  //       name: 'Menu1Demo',
-  //       component: getParentLayout('Menu1Demo'),
-  //       redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //       meta: {
-  //         title: 'Menu1'
-  //       },
-  //       children: [
-  //         {
-  //           path: 'menu1-1',
-  //           name: 'Menu11Demo',
-  //           component: getParentLayout('Menu11Demo'),
-  //           redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //           meta: {
-  //             title: 'Menu1-1',
-  //             alwaysShow: true
-  //           },
-  //           children: [
-  //             {
-  //               path: 'menu1-1-1',
-  //               name: 'Menu111Demo',
-  //               component: () => import('_v/level/Menu111.vue'),
-  //               meta: {
-  //                 title: 'Menu1-1-1'
-  //               }
-  //             }
-  //           ]
-  //         },
-  //         {
-  //           path: 'menu1-2',
-  //           name: 'Menu12Demo',
-  //           component: () => import('_v/level/Menu12.vue'),
-  //           meta: {
-  //             title: 'Menu1-2'
-  //           }
-  //         }
-  //       ]
-  //     },
-  //     {
-  //       path: 'menu2',
-  //       name: 'Menu2Demo',
-  //       component: () => import('_v/level/Menu2.vue'),
-  //       meta: {
-  //         title: 'Menu2'
-  //       }
-  //     }
-  //   ]
-  // },
+  {
+    path: '/level',
+    component: Layout,
+    redirect: '/level/menu1/menu1-1/menu1-1-1',
+    name: 'Level',
+    meta: {
+      title: '多级菜单缓存',
+      icon: 'nested'
+    },
+    children: [
+      {
+        path: 'menu1',
+        name: 'Menu1Demo',
+        component: getParentLayout('Menu1Demo'),
+        redirect: '/level/menu1/menu1-1/menu1-1-1',
+        meta: {
+          title: 'Menu1'
+        },
+        children: [
+          {
+            path: 'menu1-1',
+            name: 'Menu11Demo',
+            component: getParentLayout('Menu11Demo'),
+            redirect: '/level/menu1/menu1-1/menu1-1-1',
+            meta: {
+              title: 'Menu1-1',
+              alwaysShow: true
+            },
+            children: [
+              {
+                path: 'menu1-1-1',
+                name: 'Menu111Demo',
+                component: () => import('_v/level/Menu111.vue'),
+                meta: {
+                  title: 'Menu1-1-1'
+                }
+              }
+            ]
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu12Demo',
+            component: () => import('_v/level/Menu12.vue'),
+            meta: {
+              title: 'Menu1-2'
+            }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2Demo',
+        component: () => import('_v/level/Menu2.vue'),
+        meta: {
+          title: 'Menu2'
+        }
+      }
+    ]
+  }
   // {
   //   path: '/example-demo',
   //   component: Layout,

+ 1 - 0
src/styles/index.less

@@ -2,3 +2,4 @@
 @import './variables.less';
 @import './var.less';
 @import './sidebar.less';
+@import './transition.less';

+ 22 - 22
src/styles/sidebar.less

@@ -11,7 +11,7 @@
       background-color: var(--menu-background-color) !important;
 
       .el-menu-item,
-      .el-submenu__title {
+      .el-sub-menu__title {
         color: var(--menu-text-color) !important;
         background-color: var(--menu-background-color) !important;
 
@@ -20,9 +20,9 @@
         }
       }
 
-      .el-submenu {
+      .el-sub-menu {
         .el-menu-item,
-        .el-submenu {
+        .el-sub-menu {
           background-color: var(--sub-menu-background-color) !important;
         }
 
@@ -30,7 +30,7 @@
           color: var(--menu-active-text-color) !important;
           background-color: var(--sub-menu-hover-color) !important;
 
-          & > .el-submenu__title {
+          & > .el-sub-menu__title {
             color: var(--menu-active-text-color) !important;
           }
         }
@@ -39,14 +39,14 @@
         .el-menu {
           background-color: var(--sub-menu-background-color) !important;
 
-          .el-submenu__title {
+          .el-sub-menu__title {
             background-color: var(--sub-menu-background-color) !important;
           }
         }
       }
       // menu hover
       .submenu-title-noDropdown,
-      .el-submenu__title {
+      .el-sub-menu__title {
         &:hover {
           color: var(--sub-menu-active-text-color) !important;
           background-color: var(--menu-background-color) !important;
@@ -67,14 +67,14 @@
           background-color: var(--sub-menu-hover-color) !important;
         }
 
-        & > .el-submenu__title {
+        & > .el-sub-menu__title {
           color: var(--menu-active-text-color) !important;
         }
       }
 
       // .nest-menu {
       //   background-color: var(--sub-menu-background-color) !important;
-      //   .el-submenu>.el-submenu__title {
+      //   .el-sub-menu>.el-sub-menu__title {
       //     background-color: var(--sub-menu-background-color) !important;
       //   }
       //   .is-active {
@@ -84,14 +84,14 @@
     }
 
     .el-menu--collapse {
-      & > div > .el-submenu {
+      & > div > .el-sub-menu {
         i {
           display: none;
         }
       }
 
       .is-active {
-        & > .el-submenu__title {
+        & > .el-sub-menu__title {
           background-color: var(--sub-menu-hover-color) !important;
         }
       }
@@ -112,7 +112,7 @@
     background-color: var(--menu-background-color) !important;
 
     .el-menu-item,
-    .el-submenu__title {
+    .el-sub-menu__title {
       color: var(--menu-text-color) !important;
       background-color: var(--menu-background-color) !important;
 
@@ -125,7 +125,7 @@
       color: var(--menu-active-text-color) !important;
       background-color: var(--sub-menu-hover-color) !important;
 
-      & > .el-submenu__title {
+      & > .el-sub-menu__title {
         color: var(--menu-active-text-color) !important;
       }
     }
@@ -137,7 +137,7 @@
     // }
     // menu hover
     .submenu-title-noDropdown,
-    .el-submenu__title {
+    .el-sub-menu__title {
       &:hover {
         color: var(--sub-menu-active-text-color) !important;
         background-color: var(--menu-background-color) !important;
@@ -159,7 +159,7 @@
       background-color: var(--top-menu-background-color) !important;
 
       .el-menu-item,
-      .el-submenu__title {
+      .el-sub-menu__title {
         height: var(--top-sider-height);
         line-height: var(--top-sider-height);
         color: var(--top-menu-text-color) !important;
@@ -178,7 +178,7 @@
         color: var(--top-menu-active-text-color) !important;
         background: var(--top-menu-active-background-color) !important;
 
-        & > .el-submenu__title {
+        & > .el-sub-menu__title {
           color: var(--top-menu-active-text-color) !important;
           background: var(--top-menu-active-background-color) !important;
         }
@@ -192,7 +192,7 @@
 
       // .nest-menu {
       //   background-color: var(--sub-menu-background-color) !important;
-      //   .el-submenu>.el-submenu__title {
+      //   .el-sub-menu>.el-sub-menu__title {
       //     background-color: var(--sub-menu-background-color) !important;
       //   }
       //   .is-active {
@@ -201,7 +201,7 @@
       // }
       // menu hover
       .submenu-title-noDropdown,
-      .el-submenu__title {
+      .el-sub-menu__title {
         &:hover {
           color: var(--top-sub-menu-active-text-color) !important;
           background: var(--top-menu-active-background-color) !important;
@@ -217,15 +217,15 @@
 }
 
 .top-popper-menu {
-  margin-top: 10px;
-  margin-left: 10px;
+  // margin-top: 10px;
+  // margin-left: 10px;
   background: var(--top-menu-background-color);
 
   .el-menu {
     background-color: var(--top-menu-background-color) !important;
 
     .el-menu-item,
-    .el-submenu__title {
+    .el-sub-menu__title {
       color: var(--top-menu-text-color) !important;
       background-color: var(--top-menu-background-color) !important;
 
@@ -238,7 +238,7 @@
       color: var(--top-menu-active-text-color) !important;
       background-color: var(--top-menu-active-background-color) !important;
 
-      & > .el-submenu__title {
+      & > .el-sub-menu__title {
         color: var(--top-menu-active-text-color) !important;
         background-color: var(--top-menu-active-background-color) !important;
 
@@ -255,7 +255,7 @@
     // }
     // menu hover
     .submenu-title-noDropdown,
-    .el-submenu__title {
+    .el-sub-menu__title {
       &:hover {
         color: var(--top-sub-menu-hover-color) !important;
         background-color: var(--top-menu-background-color) !important;

+ 0 - 0
src/utils/dom-uitls.ts → src/utils/dom-utils.ts


+ 23 - 1
src/utils/validate.ts

@@ -34,7 +34,7 @@ export function isTel(tel: any): boolean {
 }
 
 // 验证数字
-export function isNum(num: any): boolean {
+export function isNumber(num: any): boolean {
   return /^[0-9]*$/.test(num)
 }
 
@@ -82,3 +82,25 @@ export const isFirefox = function () {
 export function isString(val: unknown): val is string {
   return is(val, 'String')
 }
+
+export const isWindow = (val: any): val is Window => {
+  return typeof window !== 'undefined' && is(val, 'Window')
+}
+
+export const isDef = <T = unknown>(val?: T): val is T => {
+  return typeof val !== 'undefined'
+}
+
+export const isUnDef = <T = unknown>(val?: T): val is T => {
+  return !isDef(val)
+}
+
+export const isFunction = (val: unknown): val is Function => typeof val === 'function'
+
+export const isClient = () => {
+  return typeof window !== 'undefined'
+}
+
+export const isElement = (val: unknown): val is Element => {
+  return isObject(val) && !!val.tagName
+}

+ 306 - 0
src/views/components-demo/echarts/echart-data.ts

@@ -0,0 +1,306 @@
+import { EChartsOption } from 'echarts'
+import { EChartsOption as EChartsWordOption } from 'echarts-wordcloud'
+
+export const lineOptions: EChartsOption = {
+  xAxis: {
+    data: [
+      '一月',
+      '二月',
+      '三月',
+      '四月',
+      '五月',
+      '六月',
+      '七月',
+      '八月',
+      '九月',
+      '十月',
+      '十一月',
+      '十二月'
+    ],
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 30,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  },
+  legend: {
+    data: ['预期', '实际']
+  },
+  series: [
+    {
+      name: '预期',
+      smooth: true,
+      type: 'line',
+      data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
+      animationDuration: 2800,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '实际',
+      smooth: true,
+      type: 'line',
+      itemStyle: {},
+      data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+      animationDuration: 2800,
+      animationEasing: 'quadraticOut'
+    }
+  ]
+}
+
+export const pieOptions: EChartsOption = {
+  title: {
+    text: '用户访问来源',
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left',
+    data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
+  },
+  series: [
+    {
+      name: '用户访问来源',
+      type: 'pie',
+      radius: '55%',
+      center: ['50%', '60%'],
+      data: [
+        { value: 335, name: '直接访问' },
+        { value: 310, name: '邮件营销' },
+        { value: 234, name: '联盟广告' },
+        { value: 135, name: '视频广告' },
+        { value: 1548, name: '搜索引擎' }
+      ]
+    }
+  ]
+}
+
+export const barOptions: EChartsOption = {
+  title: {
+    text: '每周用户活跃量',
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  xAxis: {
+    type: 'category',
+    data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+    axisTick: {
+      alignWithLabel: true
+    }
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '活跃量',
+      data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+      type: 'bar'
+    }
+  ]
+}
+
+export const pieOptions2: EChartsOption = {
+  title: {
+    text: '用户访问来源',
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left'
+  },
+  series: [
+    {
+      name: '访问来源',
+      type: 'pie',
+      radius: '55%',
+      center: ['50%', '50%'],
+      data: [
+        {
+          value: 335,
+          name: '直接访问'
+        },
+        {
+          value: 310,
+          name: '邮件营销'
+        },
+        {
+          value: 274,
+          name: '联盟广告'
+        },
+        {
+          value: 235,
+          name: '视频广告'
+        },
+        {
+          value: 400,
+          name: '搜索引擎'
+        }
+      ].sort(function (a, b) {
+        return a.value - b.value
+      }),
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
+        }
+      }
+    }
+  ]
+}
+
+export const wordOptions: EChartsWordOption = {
+  tooltip: {},
+  series: [
+    {
+      type: 'wordCloud',
+      gridSize: 2,
+      sizeRange: [12, 50],
+      rotationRange: [-90, 90],
+      shape: 'pentagon',
+      width: 600,
+      height: 400,
+      drawOutOfBound: true,
+      textStyle: {
+        color: function () {
+          return (
+            'rgb(' +
+            [
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160)
+            ].join(',') +
+            ')'
+          )
+        }
+      },
+      emphasis: {
+        textStyle: {
+          shadowBlur: 10,
+          shadowColor: '#333'
+        }
+      },
+      data: [
+        {
+          name: 'Sam S Club',
+          value: 10000,
+          textStyle: {
+            color: 'black'
+          },
+          emphasis: {
+            textStyle: {
+              color: 'red'
+            }
+          }
+        },
+        {
+          name: 'Macys',
+          value: 6181
+        },
+        {
+          name: 'Amy Schumer',
+          value: 4386
+        },
+        {
+          name: 'Jurassic World',
+          value: 4055
+        },
+        {
+          name: 'Charter Communications',
+          value: 2467
+        },
+        {
+          name: 'Chick Fil A',
+          value: 2244
+        },
+        {
+          name: 'Planet Fitness',
+          value: 1898
+        },
+        {
+          name: 'Pitch Perfect',
+          value: 1484
+        },
+        {
+          name: 'Express',
+          value: 1112
+        },
+        {
+          name: 'Home',
+          value: 965
+        },
+        {
+          name: 'Johnny Depp',
+          value: 847
+        },
+        {
+          name: 'Lena Dunham',
+          value: 582
+        },
+        {
+          name: 'Lewis Hamilton',
+          value: 555
+        },
+        {
+          name: 'KXAN',
+          value: 550
+        },
+        {
+          name: 'Mary Ellen Mark',
+          value: 462
+        },
+        {
+          name: 'Farrah Abraham',
+          value: 366
+        },
+        {
+          name: 'Rita Ora',
+          value: 360
+        },
+        {
+          name: 'Serena Williams',
+          value: 282
+        },
+        {
+          name: 'NCAA baseball tournament',
+          value: 273
+        },
+        {
+          name: 'Point Break',
+          value: 265
+        }
+      ]
+    }
+  ]
+}

+ 53 - 0
src/views/components-demo/echarts/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="统一封装 Echart 组件,自适应宽度,只需传入 options 与 height 属性即可展示对应的图表。"
+      type="info"
+      style="margin-bottom: 20px"
+    />
+
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart-wrap">
+          <echart :height="'300px'" :options="pieOptions" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart-wrap">
+          <echart :options="barOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart-wrap">
+          <echart :options="lineOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="10">
+        <div class="chart-wrap">
+          <echart :options="pieOptions2" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="24">
+        <div class="chart-wrap">
+          <echart :options="wordOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts" name="EchartsDemo">
+import { lineOptions, pieOptions, barOptions, pieOptions2, wordOptions } from './echart-data'
+import Echart from '_c/Echart/index.vue'
+</script>
+
+<style lang="less" scoped>
+.chart-wrap {
+  padding: 10px;
+  margin-bottom: 20px;
+  background-color: #fff;
+  border-radius: 5px;
+}
+</style>

+ 113 - 0
src/views/components-demo/preview/index.vue

@@ -0,0 +1,113 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="抽取于 Element 的图片预览组件进行改造,实现函数式调用组件,无需基于图片进行点击预览。"
+      type="info"
+      style="margin-bottom: 20px"
+    />
+
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="有底图预览。"
+      type="info"
+      style="margin-bottom: 20px"
+    />
+
+    <div class="img-wrap">
+      <div
+        v-for="(item, $index) in imgList"
+        :key="item"
+        class="img-item"
+        @click="showHasImg($index)"
+      >
+        <img :src="item" alt="" />
+      </div>
+    </div>
+
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="无底图预览。"
+      type="info"
+      style="margin-top: 20px; margin-bottom: 20px"
+    />
+    <el-button type="primary" @click="showNoImg">点击预览</el-button>
+
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="点击事件,包含图片点击事件以及关闭事件。"
+      type="info"
+      style="margin-top: 20px; margin-bottom: 20px"
+    />
+    <el-button type="primary" @click="showImg">点击预览</el-button>
+  </div>
+</template>
+
+<script setup lang="ts" name="PreviewDemo">
+import { ref } from 'vue'
+import { createImgPreview } from '_c/Preview'
+import { Message } from '_c/Message'
+
+const imgList = ref<string[]>([
+  'https://img1.baidu.com/it/u=657828739,1486746195&fm=26&fmt=auto&gp=0.jpg',
+  'https://img0.baidu.com/it/u=3114228356,677481409&fm=26&fmt=auto&gp=0.jpg',
+  'https://img1.baidu.com/it/u=508846955,3814747122&fm=26&fmt=auto&gp=0.jpg',
+  'https://img1.baidu.com/it/u=3536647690,3616605490&fm=26&fmt=auto&gp=0.jpg',
+  'https://img1.baidu.com/it/u=4087287201,1148061266&fm=26&fmt=auto&gp=0.jpg',
+  'https://img2.baidu.com/it/u=3429163260,2974496379&fm=26&fmt=auto&gp=0.jpg'
+])
+
+function showHasImg(i: number) {
+  createImgPreview({
+    index: i,
+    imageList: imgList.value
+  })
+}
+
+function showNoImg() {
+  createImgPreview({
+    index: 0,
+    imageList: imgList.value
+  })
+}
+
+function showImg() {
+  createImgPreview({
+    index: 0,
+    imageList: imgList.value,
+    onClose: (i: number) => {
+      Message.info('关闭的图片索引:' + i)
+    },
+    onSelect: (i: number) => {
+      Message.info('当前点击的图片索引:' + i)
+    }
+  })
+}
+</script>
+
+<style lang="less" scoped>
+.img-wrap {
+  display: flex;
+  justify-content: center;
+
+  .img-item {
+    position: relative;
+    width: 400px;
+    height: 300px;
+    margin: 0 10px;
+    overflow: hidden;
+    cursor: pointer;
+
+    img {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+}
+</style>

+ 147 - 0
src/views/dashboard/components/PanelGroup.vue

@@ -0,0 +1,147 @@
+<template>
+  <el-row :gutter="20" class="panel-group">
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('newVisitis')">
+        <div class="card-panel-icon-wrapper icon-people">
+          <svg-icon icon-class="peoples" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">新增用户</div>
+          <count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('messages')">
+        <div class="card-panel-icon-wrapper icon-message">
+          <svg-icon icon-class="message" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">未读信息</div>
+          <count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('purchases')">
+        <div class="card-panel-icon-wrapper icon-money">
+          <svg-icon icon-class="money" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">成交金额</div>
+          <count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('shoppings')">
+        <div class="card-panel-icon-wrapper icon-shopping">
+          <svg-icon icon-class="shopping" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">购物总量</div>
+          <count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup lang="ts" name="PanelGroup">
+import CountTo from '_c/CountTo/index.vue'
+
+const emit = defineEmits(['handleSetLineChartData'])
+
+function handleSetLineChartData(type: string) {
+  emit('handleSetLineChartData', type)
+}
+</script>
+
+<style lang="less" scoped>
+.panel-group {
+  .card-panel-col {
+    margin-bottom: 20px;
+  }
+
+  .card-panel {
+    position: relative;
+    height: 108px;
+    overflow: hidden;
+    font-size: 12px;
+    color: #666;
+    cursor: pointer;
+    background: #fff;
+    border-color: rgba(0, 0, 0, 0.05);
+    box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
+
+    &:hover {
+      .card-panel-icon-wrapper {
+        color: #fff;
+      }
+
+      .icon-people {
+        background: #40c9c6;
+      }
+
+      .icon-message {
+        background: #36a3f7;
+      }
+
+      .icon-money {
+        background: #f4516c;
+      }
+
+      .icon-shopping {
+        background: #34bfa3;
+      }
+    }
+
+    .icon-people {
+      color: #40c9c6;
+    }
+
+    .icon-message {
+      color: #36a3f7;
+    }
+
+    .icon-money {
+      color: #f4516c;
+    }
+
+    .icon-shopping {
+      color: #34bfa3;
+    }
+
+    .card-panel-icon-wrapper {
+      float: left;
+      padding: 16px;
+      margin: 14px 0 0 14px;
+      border-radius: 6px;
+      transition: all 0.38s ease-out;
+    }
+
+    .card-panel-icon {
+      float: left;
+      font-size: 48px;
+    }
+
+    .card-panel-description {
+      float: right;
+      margin: 26px;
+      margin-left: 0;
+      font-weight: bold;
+
+      .card-panel-text {
+        margin-bottom: 12px;
+        font-size: 16px;
+        line-height: 18px;
+        color: rgba(0, 0, 0, 0.45);
+      }
+
+      .card-panel-num {
+        font-size: 20px;
+      }
+    }
+  }
+}
+</style>

+ 126 - 0
src/views/dashboard/echart-data.ts

@@ -0,0 +1,126 @@
+import { EChartsOption } from 'echarts'
+
+export const lineOptions: EChartsOption = {
+  xAxis: {
+    data: [
+      '一月',
+      '二月',
+      '三月',
+      '四月',
+      '五月',
+      '六月',
+      '七月',
+      '八月',
+      '九月',
+      '十月',
+      '十一月',
+      '十二月'
+    ],
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 30,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  },
+  legend: {
+    data: ['预期', '实际']
+  },
+  series: [
+    {
+      name: '预期',
+      smooth: true,
+      type: 'line',
+      data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
+      animationDuration: 2800,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '实际',
+      smooth: true,
+      type: 'line',
+      itemStyle: {},
+      data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+      animationDuration: 2800,
+      animationEasing: 'quadraticOut'
+    }
+  ]
+}
+
+export const pieOptions: EChartsOption = {
+  title: {
+    text: '用户访问来源',
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left',
+    data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
+  },
+  series: [
+    {
+      name: '用户访问来源',
+      type: 'pie',
+      radius: '55%',
+      center: ['50%', '60%'],
+      data: [
+        { value: 335, name: '直接访问' },
+        { value: 310, name: '邮件营销' },
+        { value: 234, name: '联盟广告' },
+        { value: 135, name: '视频广告' },
+        { value: 1548, name: '搜索引擎' }
+      ]
+    }
+  ]
+}
+
+export const barOptions: EChartsOption = {
+  title: {
+    text: '每周用户活跃量',
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  xAxis: {
+    type: 'category',
+    data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+    axisTick: {
+      alignWithLabel: true
+    }
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '活跃量',
+      data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+      type: 'bar'
+    }
+  ]
+}

+ 32 - 2
src/views/dashboard/index.vue

@@ -1,5 +1,35 @@
 <template>
-  <div>2222</div>
+  <div>
+    <panel-group />
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart__wrap">
+          <echart :options="pieOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart__wrap">
+          <echart :options="barOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+    <div class="chart__wrap">
+      <echart :options="lineOptions" :height="'300px'" />
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts" name="Dashboard"></script>
+<script setup lang="ts" name="Dashboard">
+import { lineOptions, pieOptions, barOptions } from './echart-data'
+import Echart from '_c/Echart/index.vue'
+import PanelGroup from './components/PanelGroup.vue'
+</script>
+
+<style lang="less" scoped>
+.chart__wrap {
+  padding: 10px;
+  margin-bottom: 20px;
+  background-color: #fff;
+  border-radius: 5px;
+}
+</style>

+ 32 - 0
src/views/guide/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title=" 引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 intro.js"
+      type="info"
+      style="margin-bottom: 20px"
+    />
+    <el-button type="primary" @click.prevent.stop="guide"> 开始引导 </el-button>
+  </div>
+</template>
+
+<script setup lang="ts" name="Guide">
+import { onMounted } from 'vue'
+import { useIntro } from '@/hooks/web/useIntro'
+const { intro } = useIntro()
+import steps from './steps'
+
+function guide() {
+  intro.start()
+}
+
+onMounted(() => {
+  intro.addSteps(steps as any[]).setOptions({
+    prevLabel: '上一步',
+    nextLabel: '下一步',
+    skipLabel: '跳过',
+    doneLabel: '结束'
+  })
+})
+</script>

+ 40 - 0
src/views/guide/steps.ts

@@ -0,0 +1,40 @@
+const steps = [
+  {
+    element: '#sidebar__wrap',
+    title: '菜单栏',
+    intro: '以路由的结构渲染的菜单栏',
+    position: 'right'
+  },
+  {
+    element: '#hamburger-container',
+    title: '展开缩收',
+    intro: '用于展开和缩放菜单栏',
+    position: 'bottom'
+  },
+  {
+    element: '#breadcrumb-container',
+    title: '面包屑',
+    intro: '用于记录当前路由结构',
+    position: 'bottom'
+  },
+  {
+    element: '#screenfull-container',
+    title: '是否全屏',
+    intro: '用于设置是否全屏',
+    position: 'bottom'
+  },
+  {
+    element: '#user-container',
+    title: '用户信息',
+    intro: '用于展示用户',
+    position: 'bottom'
+  },
+  {
+    element: '#tag-container',
+    title: '标签页',
+    intro: '用于记录路由历史记录',
+    position: 'bottom'
+  }
+]
+
+export default steps

+ 12 - 0
src/views/level/menu111.vue

@@ -0,0 +1,12 @@
+<template>
+  <div style="display: flex; padding: 20px; background: #fff; align-items: center">
+    <div style="min-width: 200px">多层级缓存-页面1-1-1:</div>
+    <el-input v-model="value" />
+  </div>
+</template>
+
+<script setup lang="ts" name="Menu111Demo">
+import { ref } from 'vue'
+
+const value = ref<string>('')
+</script>

+ 12 - 0
src/views/level/menu12.vue

@@ -0,0 +1,12 @@
+<template>
+  <div style="display: flex; padding: 20px; background: #fff; align-items: center">
+    <div style="min-width: 200px">多层级缓存-页面1-2:</div>
+    <el-input v-model="value" />
+  </div>
+</template>
+
+<script setup lang="ts" name="Menu12Demo">
+import { ref } from 'vue'
+
+const value = ref<string>('')
+</script>

+ 12 - 0
src/views/level/menu2.vue

@@ -0,0 +1,12 @@
+<template>
+  <div style="display: flex; padding: 20px; background: #fff; align-items: center">
+    <div style="min-width: 200px">多层级缓存-页面2:</div>
+    <el-input v-model="value" />
+  </div>
+</template>
+
+<script setup lang="ts" name="Menu2Demo">
+import { ref } from 'vue'
+
+const value = ref<string>('')
+</script>

+ 30 - 0
yarn.lock

@@ -2478,6 +2478,19 @@ dot-prop@^5.1.0:
   dependencies:
     is-obj "^2.0.0"
 
+echarts-wordcloud@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/echarts-wordcloud/-/echarts-wordcloud-2.0.0.tgz#52ef817895801ffe9e99dd1bacab7686b2dec04a"
+  integrity sha512-K7l6pTklqdW7ZWzT/1CS0KhBSINr/cd7c5N1fVMzZMwLQHEwT7x+nivK7g5hkVh7WNcAv4Dn6/ZS5zMKRozC1g==
+
+echarts@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.2.1.tgz#bd58ec011cd82def4a714e4038ef4b73b8417bc3"
+  integrity sha512-OJ79b22eqRfbSV8vYmDKmA+XWfNbr0Uk/OafWcFNIGDWti2Uw9A6eVCiJLmqPa9Sk+EWL+t5v26aak0z3gxiZw==
+  dependencies:
+    tslib "2.3.0"
+    zrender "5.2.1"
+
 electron-to-chromium@^1.3.857:
   version "1.3.864"
   resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.864.tgz#6a993bcc196a2b8b3df84d28d5d4dd912393885f"
@@ -3661,6 +3674,11 @@ inquirer@6.5.2:
     strip-ansi "^5.1.0"
     through "^2.3.6"
 
+intro.js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/intro.js/-/intro.js-4.2.2.tgz#9077549cc6ef697e78d18d1c05003b7471281e1a"
+  integrity sha512-Zgz2e8syCuttJ2vJlDOWCSWPUJBr7AOJkU5Ti3zcvXho+y//q0ixwoT+PkPLJWI7AX35IdgRcxAEWUrOAJYiNQ==
+
 is-accessor-descriptor@^0.1.6:
   version "0.1.6"
   resolved "https://registry.nlark.com/is-accessor-descriptor/download/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@@ -6774,6 +6792,11 @@ ts-node@^9:
     source-map-support "^0.5.17"
     yn "3.1.1"
 
+tslib@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
+  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
 tslib@^1.8.1, tslib@^1.9.0:
   version "1.14.1"
   resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -7459,6 +7482,13 @@ yocto-queue@^0.1.0:
   resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 
+zrender@5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.2.1.tgz#5f4bbda915ba6d412b0b19dc2431beaad05417bb"
+  integrity sha512-M3bPGZuyLTNBC6LiNKXJwSCtglMp8XUEqEBG+2MdICDI3d1s500Y4P0CzldQGsqpRVB7fkvf3BKQQRxsEaTlsw==
+  dependencies:
+    tslib "2.3.0"
+
 zwitch@^1.0.0:
   version "1.0.5"
   resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"

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