diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1d3f0aa8..34310929 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -102,6 +102,7 @@
     "codemirror",
     "commitlint",
     "cropperjs",
+    "echart",
     "echarts",
     "esnext",
     "esno",
@@ -116,10 +117,12 @@
     "sider",
     "sortablejs",
     "stylelint",
+    "svgs",
     "unocss",
     "unplugin",
     "unref",
     "videojs",
+    "VITE",
     "vitejs",
     "vueuse",
     "wangeditor",
diff --git a/package.json b/package.json
index 21a731a8..fb0f23ad 100644
--- a/package.json
+++ b/package.json
@@ -36,20 +36,20 @@
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^3.0.4",
     "animate.css": "^4.1.1",
-    "axios": "^1.5.1",
+    "axios": "^1.6.0",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.10.0",
     "camunda-bpmn-moddle": "^7.0.1",
     "cropperjs": "^1.6.1",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.10",
-    "diagram-js": "^12.5.0",
+    "diagram-js": "^12.6.0",
+    "driver.js": "^1.3.0",
     "echarts": "^5.4.3",
     "echarts-wordcloud": "^2.1.0",
     "element-plus": "2.4.1",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
-    "intro.js": "^7.2.0",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
     "min-dash": "^4.1.1",
@@ -64,7 +64,7 @@
     "video.js": "^7.21.5",
     "vue": "^3.3.7",
     "vue-dompurify-html": "^4.1.4",
-    "vue-i18n": "^9.5.0",
+    "vue-i18n": "^9.6.2",
     "vue-router": "^4.2.5",
     "vue-types": "^5.1.1",
     "vuedraggable": "^4.1.0",
@@ -72,20 +72,19 @@
     "xml-js": "^1.6.11"
   },
   "devDependencies": {
-    "@commitlint/cli": "^18.0.0",
-    "@commitlint/config-conventional": "^18.0.0",
-    "@iconify/json": "^2.2.132",
+    "@commitlint/cli": "^18.2.0",
+    "@commitlint/config-conventional": "^18.1.0",
+    "@iconify/json": "^2.2.135",
     "@intlify/unplugin-vue-i18n": "^1.4.0",
     "@purge-icons/generated": "^0.9.0",
-    "@types/intro.js": "^5.1.3",
     "@types/lodash-es": "^4.17.10",
-    "@types/node": "^20.8.8",
+    "@types/node": "^20.8.9",
     "@types/nprogress": "^0.2.2",
     "@types/qrcode": "^1.5.4",
     "@types/qs": "^6.9.9",
     "@types/sortablejs": "^1.15.4",
-    "@typescript-eslint/eslint-plugin": "^6.9.0",
-    "@typescript-eslint/parser": "^6.9.0",
+    "@typescript-eslint/eslint-plugin": "^6.9.1",
+    "@typescript-eslint/parser": "^6.9.1",
     "@unocss/transformer-variant-group": "^0.57.1",
     "@unocss/eslint-config": "^0.57.1",
     "@vitejs/plugin-legacy": "^4.1.1",
@@ -99,24 +98,24 @@
     "eslint-config-prettier": "^9.0.0",
     "eslint-define-config": "^1.24.1",
     "eslint-plugin-prettier": "^5.0.1",
-    "eslint-plugin-vue": "^9.18.0",
+    "eslint-plugin-vue": "^9.18.1",
     "lint-staged": "^15.0.2",
     "postcss": "^8.4.31",
     "postcss-html": "^1.5.0",
     "postcss-scss": "^4.0.9",
     "prettier": "^3.0.3",
     "rimraf": "^5.0.5",
-    "rollup": "^4.1.4",
-    "sass": "^1.69.4",
+    "rollup": "^4.1.5",
+    "sass": "^1.69.5",
     "stylelint": "^15.11.0",
     "stylelint-config-html": "^1.1.0",
     "stylelint-config-recommended": "^13.0.0",
     "stylelint-config-standard": "^34.0.0",
     "stylelint-order": "^6.0.3",
-    "terser": "^5.22.0",
+    "terser": "^5.23.0",
     "typescript": "5.2.2",
     "unocss": "^0.57.1",
-    "unplugin-auto-import": "^0.16.6",
+    "unplugin-auto-import": "^0.16.7",
     "unplugin-element-plus": "^0.8.0",
     "unplugin-vue-components": "^0.25.2",
     "vite": "4.5.0",
@@ -128,7 +127,7 @@
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-top-level-await": "^1.3.1",
     "vue-eslint-parser": "^9.3.2",
-    "vue-tsc": "^1.8.20"
+    "vue-tsc": "^1.8.22"
   },
   "license": "MIT",
   "repository": {
diff --git a/src/components/Card/src/CardTitle.vue b/src/components/Card/src/CardTitle.vue
index 5b122f49..76a83564 100644
--- a/src/components/Card/src/CardTitle.vue
+++ b/src/components/Card/src/CardTitle.vue
@@ -3,7 +3,7 @@ defineComponent({
   name: 'CardTitle'
 })
 
-const { title } = defineProps({
+defineProps({
   title: {
     type: String,
     required: true
diff --git a/src/components/ConfigGlobal/src/ConfigGlobal.vue b/src/components/ConfigGlobal/src/ConfigGlobal.vue
index a0873967..5bd90cf1 100644
--- a/src/components/ConfigGlobal/src/ConfigGlobal.vue
+++ b/src/components/ConfigGlobal/src/ConfigGlobal.vue
@@ -1,20 +1,19 @@
-<script lang="ts" setup>
+<script setup lang="ts">
+import { provide, computed, watch, onMounted } from 'vue'
 import { propTypes } from '@/utils/propTypes'
+import { ComponentSize, ElConfigProvider } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
+import { useWindowSize } from '@vueuse/core'
 import { useAppStore } from '@/store/modules/app'
 import { setCssVar } from '@/utils'
 import { useDesign } from '@/hooks/web/useDesign'
-import { ElementPlusSize } from '@/types/elementPlus'
-import { useWindowSize } from '@vueuse/core'
-
-defineOptions({ name: 'ConfigGlobal' })
 
 const { variables } = useDesign()
 
 const appStore = useAppStore()
 
 const props = defineProps({
-  size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
+  size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
 })
 
 provide('configGlobal', props)
@@ -53,9 +52,9 @@ const currentLocale = computed(() => localeStore.currentLocale)
 
 <template>
   <ElConfigProvider
+    :namespace="variables.elNamespace"
     :locale="currentLocale.elLocale"
     :message="{ max: 1 }"
-    :namespace="variables.elNamespace"
     :size="size"
   >
     <slot></slot>
diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue
index d7fa90cb..9f268a3f 100644
--- a/src/components/ShortcutDateRangePicker/index.vue
+++ b/src/components/ShortcutDateRangePicker/index.vue
@@ -27,7 +27,7 @@ import * as DateUtil from '@/utils/formatTime'
 defineOptions({ name: 'ShortcutDateRangePicker' })
 
 const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
-const times = ref<[dayjs.ConfigType, dayjs.ConfigType]>(['', '']) // 时间范围参数
+const times = ref<[string, string]>(['', '']) // 时间范围参数
 defineExpose({ times }) // 暴露时间范围参数
 /** 日期快捷选择 */
 const shortcuts = [
diff --git a/src/components/Sticky/src/Sticky.vue b/src/components/Sticky/src/Sticky.vue
index b958544a..28ecbcb8 100644
--- a/src/components/Sticky/src/Sticky.vue
+++ b/src/components/Sticky/src/Sticky.vue
@@ -32,7 +32,7 @@ onMounted(() => {
 
   scrollContainer.value = getScrollContainer(refSticky.value!, true)
   useEventListener(scrollContainer, 'scroll', handleScroll)
-  useEventListener('resize', handleReize)
+  useEventListener('resize', handleResize)
   handleScroll()
 })
 onActivated(() => {
@@ -103,7 +103,7 @@ const handleScroll = () => {
     reset()
   }
 }
-const handleReize = () => {
+const handleResize = () => {
   if (isSticky.value && refSticky.value) {
     width.value = refSticky.value.getBoundingClientRect().width + 'px'
   }
diff --git a/src/components/Tooltip/src/Tooltip.vue b/src/components/Tooltip/src/Tooltip.vue
index 7490bd70..1a2e09cc 100644
--- a/src/components/Tooltip/src/Tooltip.vue
+++ b/src/components/Tooltip/src/Tooltip.vue
@@ -4,13 +4,13 @@ import { propTypes } from '@/utils/propTypes'
 defineOptions({ name: 'Tooltip' })
 
 defineProps({
-  titel: propTypes.string.def(''),
+  title: propTypes.string.def(''),
   message: propTypes.string.def(''),
   icon: propTypes.string.def('ep:question-filled')
 })
 </script>
 <template>
-  <span>{{ titel }}</span>
+  <span>{{ title }}</span>
   <ElTooltip :content="message" placement="top">
     <Icon :icon="icon" class="relative top-1px ml-1px" />
   </ElTooltip>
diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts
new file mode 100644
index 00000000..7fd2fb09
--- /dev/null
+++ b/src/hooks/web/useGuide.ts
@@ -0,0 +1,49 @@
+import { Config, driver } from 'driver.js'
+import 'driver.js/dist/driver.css'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const { t } = useI18n()
+
+const { variables } = useDesign()
+
+export const useGuide = (options?: Config) => {
+  const driverObj = driver(
+    options || {
+      showProgress: true,
+      nextBtnText: t('common.nextLabel'),
+      prevBtnText: t('common.prevLabel'),
+      doneBtnText: t('common.doneLabel'),
+      steps: [
+        {
+          element: `#${variables.namespace}-menu`,
+          popover: {
+            title: t('common.menu'),
+            description: t('common.menuDes'),
+            side: 'right'
+          }
+        },
+        {
+          element: `#${variables.namespace}-tool-header`,
+          popover: {
+            title: t('common.tool'),
+            description: t('common.toolDes'),
+            side: 'left'
+          }
+        },
+        {
+          element: `#${variables.namespace}-tags-view`,
+          popover: {
+            title: t('common.tagsView'),
+            description: t('common.tagsViewDes'),
+            side: 'bottom'
+          }
+        }
+      ]
+    }
+  )
+
+  return {
+    ...driverObj
+  }
+}
diff --git a/src/hooks/web/useIntro.ts b/src/hooks/web/useIntro.ts
deleted file mode 100644
index 7fe00845..00000000
--- a/src/hooks/web/useIntro.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import introJs from 'intro.js'
-import { IntroJs, Step, Options } from 'intro.js'
-import 'intro.js/introjs.css'
-
-import { useDesign } from '@/hooks/web/useDesign'
-
-export const useIntro = (setps?: Step[], options?: Options) => {
-  const { t } = useI18n()
-
-  const { variables } = useDesign()
-
-  const defaultSetps: Step[] = setps || [
-    {
-      element: `#${variables.namespace}-menu`,
-      title: t('common.menu'),
-      intro: t('common.menuDes'),
-      position: 'right'
-    },
-    {
-      element: `#${variables.namespace}-tool-header`,
-      title: t('common.tool'),
-      intro: t('common.toolDes'),
-      position: 'left'
-    },
-    {
-      element: `#${variables.namespace}-tags-view`,
-      title: t('common.tagsView'),
-      intro: t('common.tagsViewDes'),
-      position: 'bottom'
-    }
-  ]
-
-  const defaultOptions: Options = options || {
-    prevLabel: t('common.prevLabel'),
-    nextLabel: t('common.nextLabel'),
-    skipLabel: t('common.skipLabel'),
-    doneLabel: t('common.doneLabel')
-  }
-
-  const introRef: IntroJs = introJs()
-
-  introRef.addSteps(defaultSetps).setOptions(defaultOptions)
-
-  return {
-    introRef
-  }
-}
diff --git a/src/hooks/web/useNetwork.ts b/src/hooks/web/useNetwork.ts
new file mode 100644
index 00000000..66fa4464
--- /dev/null
+++ b/src/hooks/web/useNetwork.ts
@@ -0,0 +1,21 @@
+import { ref, onBeforeUnmount } from 'vue'
+
+const useNetwork = () => {
+  const online = ref(true)
+
+  const updateNetwork = () => {
+    online.value = navigator.onLine
+  }
+
+  window.addEventListener('online', updateNetwork)
+  window.addEventListener('offline', updateNetwork)
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('online', updateNetwork)
+    window.removeEventListener('offline', updateNetwork)
+  })
+
+  return { online }
+}
+
+export { useNetwork }
diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts
new file mode 100644
index 00000000..09d3176b
--- /dev/null
+++ b/src/hooks/web/useNow.ts
@@ -0,0 +1,60 @@
+import { dateUtil } from '@/utils/dateUtil'
+import { reactive, toRefs } from 'vue'
+import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
+
+export const useNow = (immediate = true) => {
+  let timer: IntervalHandle
+
+  const state = reactive({
+    year: 0,
+    month: 0,
+    week: '',
+    day: 0,
+    hour: '',
+    minute: '',
+    second: 0,
+    meridiem: ''
+  })
+
+  const update = () => {
+    const now = dateUtil()
+
+    const h = now.format('HH')
+    const m = now.format('mm')
+    const s = now.get('s')
+
+    state.year = now.get('y')
+    state.month = now.get('M') + 1
+    state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
+    state.day = now.get('date')
+    state.hour = h
+    state.minute = m
+    state.second = s
+
+    state.meridiem = now.format('A')
+  }
+
+  function start() {
+    update()
+    clearInterval(timer)
+    timer = setInterval(() => update(), 1000)
+  }
+
+  function stop() {
+    clearInterval(timer)
+  }
+
+  tryOnMounted(() => {
+    immediate && start()
+  })
+
+  tryOnUnmounted(() => {
+    stop()
+  })
+
+  return {
+    ...toRefs(state),
+    start,
+    stop
+  }
+}
diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts
new file mode 100644
index 00000000..31eadb02
--- /dev/null
+++ b/src/hooks/web/useTagsView.ts
@@ -0,0 +1,63 @@
+import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
+import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
+import { computed, nextTick, unref } from 'vue'
+
+export const useTagsView = () => {
+  const tagsViewStore = useTagsViewStoreWithOut()
+
+  const { replace, currentRoute } = useRouter()
+
+  const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+  const closeAll = (callback?: Fn) => {
+    tagsViewStore.delAllViews()
+    callback?.()
+  }
+
+  const closeLeft = (callback?: Fn) => {
+    tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeRight = (callback?: Fn) => {
+    tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeOther = (callback?: Fn) => {
+    tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+    if (view?.meta?.affix) return
+    tagsViewStore.delView(view || unref(currentRoute))
+
+    callback?.()
+  }
+
+  const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+    tagsViewStore.delCachedView()
+    const { path, query } = view || unref(currentRoute)
+    await nextTick()
+    replace({
+      path: '/redirect' + path,
+      query: query
+    })
+    callback?.()
+  }
+
+  const setTitle = (title: string, path?: string) => {
+    tagsViewStore.setTitle(title, path)
+  }
+
+  return {
+    closeAll,
+    closeLeft,
+    closeRight,
+    closeOther,
+    closeCurrent,
+    refreshPage,
+    setTitle
+  }
+}
diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts
index 0c16fa31..151e35b2 100644
--- a/src/hooks/web/useValidator.ts
+++ b/src/hooks/web/useValidator.ts
@@ -1,54 +1,53 @@
-const { t } = useI18n()
+import { useI18n } from '@/hooks/web/useI18n'
+import { FormItemRule } from 'element-plus'
 
-type Callback = (error?: string | Error | undefined) => void
+const { t } = useI18n()
 
 interface LengthRange {
   min: number
   max: number
-  message: string
+  message?: string
 }
 
 export const useValidator = () => {
-  const required = (message?: string) => {
+  const required = (message?: string): FormItemRule => {
     return {
       required: true,
       message: message || t('common.required')
     }
   }
 
-  const lengthRange = (val: any, callback: Callback, options: LengthRange) => {
+  const lengthRange = (options: LengthRange): FormItemRule => {
     const { min, max, message } = options
-    if (val.length < min || val.length > max) {
-      callback(new Error(message))
-    } else {
-      callback()
+
+    return {
+      min,
+      max,
+      message: message || t('common.lengthRange', { min, max })
     }
   }
 
-  const notSpace = (val: any, callback: Callback, message: string) => {
-    // 用户名不能有空格
-    if (val.indexOf(' ') !== -1) {
-      callback(new Error(message))
-    } else {
-      callback()
+  const notSpace = (message?: string): FormItemRule => {
+    return {
+      validator: (_, val, callback) => {
+        if (val?.indexOf(' ') !== -1) {
+          callback(new Error(message || t('common.notSpace')))
+        } else {
+          callback()
+        }
+      }
     }
   }
 
-  const notSpecialCharacters = (val: any, callback: Callback, message: string) => {
-    // 密码不能是特殊字符
-    if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
-      callback(new Error(message))
-    } else {
-      callback()
-    }
-  }
-
-  // 两个字符串是否想等
-  const isEqual = (val1: string, val2: string, callback: Callback, message: string) => {
-    if (val1 === val2) {
-      callback()
-    } else {
-      callback(new Error(message))
+  const notSpecialCharacters = (message?: string): FormItemRule => {
+    return {
+      validator: (_, val, callback) => {
+        if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
+          callback(new Error(message || t('common.notSpecialCharacters')))
+        } else {
+          callback()
+        }
+      }
     }
   }
 
@@ -56,7 +55,6 @@ export const useValidator = () => {
     required,
     lengthRange,
     notSpace,
-    notSpecialCharacters,
-    isEqual
+    notSpecialCharacters
   }
 }
diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts
index 53ccda11..e2ffcadd 100644
--- a/src/utils/formatTime.ts
+++ b/src/utils/formatTime.ts
@@ -334,6 +334,6 @@ export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
 export function getDateRange(
   beginDate: dayjs.ConfigType,
   endDate: dayjs.ConfigType
-): [dayjs.ConfigType, dayjs.ConfigType] {
-  return [dayjs(beginDate).startOf('d'), dayjs(endDate).endOf('d')]
+): [string, string] {
+  return [dayjs(beginDate).startOf('d').toString(), dayjs(endDate).endOf('d').toString()]
 }
diff --git a/src/utils/index.ts b/src/utils/index.ts
index d5301ddb..10f57567 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -34,6 +34,13 @@ export const underlineToHump = (str: string): string => {
   })
 }
 
+/**
+ * 驼峰转横杠
+ */
+export const humpToDash = (str: string): string => {
+  return str.replace(/([A-Z])/g, '-$1').toLowerCase()
+}
+
 export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
   dom.style.setProperty(prop, val)
 }
@@ -67,7 +74,7 @@ export const trim = (str: string) => {
  * @param {Date | number | string} time 需要转换的时间
  * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
  */
-export const formatTime = (time: Date | number | string, fmt: string) => {
+export function formatTime(time: Date | number | string, fmt: string) {
   if (!time) return ''
   else {
     const date = new Date(time)
@@ -98,7 +105,7 @@ export const formatTime = (time: Date | number | string, fmt: string) => {
 /**
  * 生成随机字符串
  */
-export const toAnyString = () => {
+export function toAnyString() {
   const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
     const r: number = (Math.random() * 16) | 0
     const v: number = c === 'x' ? r : (r & 0x3) | 0x8
@@ -107,6 +114,13 @@ export const toAnyString = () => {
   return str
 }
 
+/**
+ * 首字母大写
+ */
+export function firstUpperCase(str: string) {
+  return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
+}
+
 export const generateUUID = () => {
   if (typeof crypto === 'object') {
     if (typeof crypto.randomUUID === 'function') {
diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue
index 78debc55..7eaf6ab6 100644
--- a/src/views/system/menu/MenuForm.vue
+++ b/src/views/system/menu/MenuForm.vue
@@ -38,7 +38,7 @@
         <template #label>
           <Tooltip
             message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
-            titel="路由地址"
+            title="路由地址"
           />
         </template>
         <el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
@@ -53,7 +53,7 @@
         <template #label>
           <Tooltip
             message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
-            titel="权限标识"
+            title="权限标识"
           />
         </template>
         <el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
@@ -74,7 +74,7 @@
       </el-form-item>
       <el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
         <template #label>
-          <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" titel="显示状态" />
+          <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
         </template>
         <el-radio-group v-model="formData.visible">
           <el-radio key="true" :label="true" border>显示</el-radio>
@@ -85,7 +85,7 @@
         <template #label>
           <Tooltip
             message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
-            titel="总是显示"
+            title="总是显示"
           />
         </template>
         <el-radio-group v-model="formData.alwaysShow">
@@ -97,7 +97,7 @@
         <template #label>
           <Tooltip
             message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
-            titel="缓存状态"
+            title="缓存状态"
           />
         </template>
         <el-radio-group v-model="formData.keepAlive">
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 2f564dd9..bf64a807 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -1,6 +1,6 @@
 <template>
   <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
-  <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue2/route/" />
+  <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
diff --git a/tsconfig.json b/tsconfig.json
index 1ee23774..182852ac 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -27,7 +27,6 @@
       "@intlify/unplugin-vue-i18n/types",
       "vite/client",
       "element-plus/global",
-      "@types/intro.js",
       "@types/qrcode",
       "vite-plugin-svg-icons/client"
     ],
diff --git a/uno.config.ts b/uno.config.ts
index 0645fe68..8b874a4a 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -101,5 +101,8 @@ ${selector}:after {
     ]
   ],
   presets: [presetUno({ dark: 'class', attributify: false })],
-  transformers: [transformerVariantGroup()]
+  transformers: [transformerVariantGroup()],
+  shortcuts: {
+    'wh-full': 'w-full h-full'
+  }
 })