Full Code of HalseySpicy/Geeker-Admin for AI

master d05424c2acda cached
278 files
1.5 MB
515.1k tokens
155 symbols
1 requests
Download .txt
Showing preview only (1,623K chars total). Download the full file or copy to clipboard to get everything.
Repository: HalseySpicy/Geeker-Admin
Branch: master
Commit: d05424c2acda
Files: 278
Total size: 1.5 MB

Directory structure:
gitextract_l6iyifel/

├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .husky/
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.cjs
├── .stylelintignore
├── .stylelintrc.cjs
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build/
│   ├── getEnv.ts
│   ├── plugins.ts
│   └── proxy.ts
├── commitlint.config.cjs
├── index.html
├── lint-staged.config.cjs
├── package.json
├── postcss.config.cjs
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── config/
│   │   │   └── servicePort.ts
│   │   ├── helper/
│   │   │   ├── axiosCancel.ts
│   │   │   └── checkStatus.ts
│   │   ├── index.ts
│   │   ├── interface/
│   │   │   └── index.ts
│   │   └── modules/
│   │       ├── login.ts
│   │       ├── upload.ts
│   │       └── user.ts
│   ├── assets/
│   │   ├── fonts/
│   │   │   ├── DIN.otf
│   │   │   └── font.scss
│   │   ├── iconfont/
│   │   │   └── iconfont.scss
│   │   └── json/
│   │       ├── authButtonList.json
│   │       └── authMenuList.json
│   ├── components/
│   │   ├── ECharts/
│   │   │   ├── config/
│   │   │   │   └── index.ts
│   │   │   └── index.vue
│   │   ├── ErrorMessage/
│   │   │   ├── 403.vue
│   │   │   ├── 404.vue
│   │   │   ├── 500.vue
│   │   │   └── index.scss
│   │   ├── Grid/
│   │   │   ├── components/
│   │   │   │   └── GridItem.vue
│   │   │   ├── index.vue
│   │   │   └── interface/
│   │   │       └── index.ts
│   │   ├── ImportExcel/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── Loading/
│   │   │   ├── fullScreen.ts
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── ProTable/
│   │   │   ├── components/
│   │   │   │   ├── ColSetting.vue
│   │   │   │   ├── Pagination.vue
│   │   │   │   └── TableColumn.vue
│   │   │   ├── index.vue
│   │   │   └── interface/
│   │   │       └── index.ts
│   │   ├── SearchForm/
│   │   │   ├── components/
│   │   │   │   └── SearchFormItem.vue
│   │   │   └── index.vue
│   │   ├── SelectFilter/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── SelectIcon/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── SvgIcon/
│   │   │   └── index.vue
│   │   ├── SwitchDark/
│   │   │   └── index.vue
│   │   ├── TreeFilter/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── Upload/
│   │   │   ├── Img.vue
│   │   │   └── Imgs.vue
│   │   └── WangEditor/
│   │       ├── index.scss
│   │       └── index.vue
│   ├── config/
│   │   ├── index.ts
│   │   └── nprogress.ts
│   ├── directives/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── auth.ts
│   │       ├── copy.ts
│   │       ├── debounce.ts
│   │       ├── draggable.ts
│   │       ├── longpress.ts
│   │       ├── throttle.ts
│   │       └── waterMarker.ts
│   ├── enums/
│   │   └── httpEnum.ts
│   ├── hooks/
│   │   ├── interface/
│   │   │   └── index.ts
│   │   ├── useAuthButtons.ts
│   │   ├── useDownload.ts
│   │   ├── useHandleData.ts
│   │   ├── useOnline.ts
│   │   ├── useSelection.ts
│   │   ├── useTable.ts
│   │   ├── useTheme.ts
│   │   └── useTime.ts
│   ├── languages/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── en.ts
│   │       └── zh.ts
│   ├── layouts/
│   │   ├── LayoutClassic/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutColumns/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutTransverse/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutVertical/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── components/
│   │   │   ├── Footer/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── Header/
│   │   │   │   ├── ToolBarLeft.vue
│   │   │   │   ├── ToolBarRight.vue
│   │   │   │   └── components/
│   │   │   │       ├── AssemblySize.vue
│   │   │   │       ├── Avatar.vue
│   │   │   │       ├── Breadcrumb.vue
│   │   │   │       ├── CollapseIcon.vue
│   │   │   │       ├── Fullscreen.vue
│   │   │   │       ├── InfoDialog.vue
│   │   │   │       ├── Language.vue
│   │   │   │       ├── Message.vue
│   │   │   │       ├── PasswordDialog.vue
│   │   │   │       ├── SearchMenu.vue
│   │   │   │       └── ThemeSetting.vue
│   │   │   ├── Main/
│   │   │   │   ├── components/
│   │   │   │   │   └── Maximize.vue
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── Menu/
│   │   │   │   └── SubMenu.vue
│   │   │   ├── Tabs/
│   │   │   │   ├── components/
│   │   │   │   │   └── MoreButton.vue
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── ThemeDrawer/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── index.vue
│   │   └── indexAsync.vue
│   ├── main.ts
│   ├── routers/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── dynamicRouter.ts
│   │       └── staticRouter.ts
│   ├── stores/
│   │   ├── helper/
│   │   │   └── persist.ts
│   │   ├── index.ts
│   │   ├── interface/
│   │   │   └── index.ts
│   │   └── modules/
│   │       ├── auth.ts
│   │       ├── global.ts
│   │       ├── keepAlive.ts
│   │       ├── tabs.ts
│   │       └── user.ts
│   ├── styles/
│   │   ├── common.scss
│   │   ├── element-dark.scss
│   │   ├── element.scss
│   │   ├── reset.scss
│   │   ├── theme/
│   │   │   ├── aside.ts
│   │   │   ├── header.ts
│   │   │   └── menu.ts
│   │   └── var.scss
│   ├── typings/
│   │   ├── global.d.ts
│   │   ├── utils.d.ts
│   │   └── window.d.ts
│   ├── utils/
│   │   ├── color.ts
│   │   ├── dict.ts
│   │   ├── eleValidate.ts
│   │   ├── errorHandler.ts
│   │   ├── index.ts
│   │   ├── is/
│   │   │   └── index.ts
│   │   ├── mittBus.ts
│   │   └── svg.ts
│   ├── views/
│   │   ├── about/
│   │   │   └── index.vue
│   │   ├── assembly/
│   │   │   ├── batchImport/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── draggable/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── guide/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── selectFilter/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── selectIcon/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── svgIcon/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── tabs/
│   │   │   │   ├── detail.vue
│   │   │   │   └── index.vue
│   │   │   ├── treeFilter/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── uploadFile/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── wangEditor/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── auth/
│   │   │   ├── button/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── menu/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── dashboard/
│   │   │   └── dataVisualize/
│   │   │       ├── components/
│   │   │       │   ├── curve.vue
│   │   │       │   └── pie.vue
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── dataScreen/
│   │   │   ├── assets/
│   │   │   │   ├── alarmList.Json
│   │   │   │   ├── china.json
│   │   │   │   └── ranking-icon.ts
│   │   │   ├── components/
│   │   │   │   ├── AgeRatioChart.vue
│   │   │   │   ├── AnnualUseChart.vue
│   │   │   │   ├── ChinaMapChart.vue
│   │   │   │   ├── HotPlateChart.vue
│   │   │   │   ├── MaleFemaleRatioChart.vue
│   │   │   │   ├── OverNext30Chart.vue
│   │   │   │   ├── PlatformSourceChart.vue
│   │   │   │   └── RealTimeAccessChart.vue
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── directives/
│   │   │   ├── copyDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── debounceDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── dragDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── longpressDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── throttleDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── watermarkDirect/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── echarts/
│   │   │   ├── columnChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── lineChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── nestedChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── pieChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── radarChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── waterChart/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── form/
│   │   │   ├── basicForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── dynamicForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── proForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── validateForm/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── home/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── link/
│   │   │   ├── bing/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── docs/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── gitee/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── github/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── juejin/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── login/
│   │   │   ├── components/
│   │   │   │   └── LoginForm.vue
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── menu/
│   │   │   ├── menu1/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── menu2/
│   │   │   │   ├── menu21/
│   │   │   │   │   ├── index.scss
│   │   │   │   │   └── index.vue
│   │   │   │   ├── menu22/
│   │   │   │   │   ├── menu221/
│   │   │   │   │   │   ├── index.scss
│   │   │   │   │   │   └── index.vue
│   │   │   │   │   └── menu222/
│   │   │   │   │       ├── index.scss
│   │   │   │   │       └── index.vue
│   │   │   │   └── menu23/
│   │   │   │       ├── index.scss
│   │   │   │       └── index.vue
│   │   │   └── menu3/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── proTable/
│   │   │   ├── complexProTable/
│   │   │   │   └── index.vue
│   │   │   ├── components/
│   │   │   │   └── UserDrawer.vue
│   │   │   ├── document/
│   │   │   │   └── index.vue
│   │   │   ├── treeProTable/
│   │   │   │   └── index.vue
│   │   │   ├── useProTable/
│   │   │   │   ├── detail.vue
│   │   │   │   └── index.vue
│   │   │   ├── useSelectFilter/
│   │   │   │   └── index.vue
│   │   │   └── useTreeFilter/
│   │   │       ├── detail.vue
│   │   │       └── index.vue
│   │   └── system/
│   │       ├── accountManage/
│   │       │   └── index.vue
│   │       ├── departmentManage/
│   │       │   └── index.vue
│   │       ├── dictManage/
│   │       │   └── index.vue
│   │       ├── menuMange/
│   │       │   └── index.vue
│   │       ├── roleManage/
│   │       │   └── index.vue
│   │       ├── systemLog/
│   │       │   └── index.vue
│   │       └── timingTask/
│   │           └── index.vue
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# @see: http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
max_line_length = 130 # 最大行长度

[*.md] # 表示仅对 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪


================================================
FILE: .eslintignore
================================================
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
/src/mock/*
stats.html


================================================
FILE: .eslintrc.cjs
================================================
// @see: http://eslint.cn

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es6: true
  },
  // 指定如何解析语法
  parser: "vue-eslint-parser",
  // 优先级低于 parse 的语法解析配置
  parserOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: 2020,
    sourceType: "module",
    jsxPragma: "React",
    ecmaFeatures: {
      jsx: true
    }
  },
  // 继承某些已有的规则
  extends: ["plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
  /**
   * "off" 或 0    ==>  关闭规则
   * "warn" 或 1   ==>  打开的规则作为警告(不影响代码执行)
   * "error" 或 2  ==>  规则作为一个错误(代码不能执行,界面报错)
   */
  rules: {
    // eslint (http://eslint.cn/docs/rules)
    "no-var": "error", // 要求使用 let 或 const 而不是 var
    "no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行
    "prefer-const": "off", // 使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
    "no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们

    // typeScript (https://typescript-eslint.io/rules)
    "@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量
    "@typescript-eslint/no-empty-function": "error", // 禁止空函数
    "@typescript-eslint/prefer-ts-expect-error": "error", // 禁止使用 @ts-ignore
    "@typescript-eslint/ban-ts-comment": "error", // 禁止 @ts-<directive> 使用注释或要求在指令后进行描述
    "@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长
    "@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间
    "@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
    "@typescript-eslint/ban-types": "off", // 禁止使用特定类型
    "@typescript-eslint/no-var-requires": "off", // 允许使用 require() 函数导入模块
    "@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!)

    // vue (https://eslint.vuejs.org/rules)
    "vue/script-setup-uses-vars": "error", // 防止<script setup>使用的变量<template>被标记为未使用,此规则仅在启用该 no-unused-vars 规则时有效
    "vue/v-slot-style": "error", // 强制执行 v-slot 指令样式
    "vue/no-mutating-props": "error", // 不允许改变组件 prop
    "vue/custom-event-name-casing": "error", // 为自定义事件名称强制使用特定大小写
    "vue/html-closing-bracket-newline": "error", // 在标签的右括号之前要求或禁止换行
    "vue/attribute-hyphenation": "error", // 对模板中的自定义组件强制执行属性命名样式:my-prop="prop"
    "vue/attributes-order": "off", // vue api使用顺序,强制执行属性顺序
    "vue/no-v-html": "off", // 禁止使用 v-html
    "vue/require-default-prop": "off", // 此规则要求为每个 prop 为必填时,必须提供默认值
    "vue/multi-word-component-names": "off", // 要求组件名称始终为 “-” 链接的单词
    "vue/no-setup-props-destructure": "off" // 禁止解构 props 传递给 setup
  }
};


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
stats.html
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install commitlint --edit $1


================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint:lint-staged


================================================
FILE: .prettierignore
================================================
/dist/*
.local
/node_modules/**

**/*.svg
**/*.sh

/public/*
stats.html


================================================
FILE: .prettierrc.cjs
================================================
// @see: https://www.prettier.cn

module.exports = {
  // 指定最大换行长度
  printWidth: 130,
  // 缩进制表符宽度 | 空格数
  tabWidth: 2,
  // 使用制表符而不是空格缩进行 (true:制表符,false:空格)
  useTabs: false,
  // 结尾不用分号 (true:有,false:没有)
  semi: true,
  // 使用单引号 (true:单引号,false:双引号)
  singleQuote: false,
  // 在对象字面量中决定是否将属性名用引号括起来 可选值 "<as-needed|consistent|preserve>"
  quoteProps: "as-needed",
  // 在JSX中使用单引号而不是双引号 (true:单引号,false:双引号)
  jsxSingleQuote: false,
  // 多行时尽可能打印尾随逗号 可选值"<none|es5|all>"
  trailingComma: "none",
  // 在对象,数组括号与文字之间加空格 "{ foo: bar }" (true:有,false:没有)
  bracketSpacing: true,
  // 将 > 多行元素放在最后一行的末尾,而不是单独放在下一行 (true:放末尾,false:单独一行)
  bracketSameLine: false,
  // (x) => {} 箭头函数参数只有一个时是否要有小括号 (avoid:省略括号,always:不省略括号)
  arrowParens: "avoid",
  // 指定要使用的解析器,不需要写文件开头的 @prettier
  requirePragma: false,
  // 可以在文件顶部插入一个特殊标记,指定该文件已使用 Prettier 格式化
  insertPragma: false,
  // 用于控制文本是否应该被换行以及如何进行换行
  proseWrap: "preserve",
  // 在html中空格是否是敏感的 "css" - 遵守 CSS 显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的
  htmlWhitespaceSensitivity: "css",
  // 控制在 Vue 单文件组件中 <script> 和 <style> 标签内的代码缩进方式
  vueIndentScriptAndStyle: false,
  // 换行符使用 lf 结尾是 可选值 "<auto|lf|crlf|cr>"
  endOfLine: "auto",
  // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 (rangeStart:开始,rangeEnd:结束)
  rangeStart: 0,
  rangeEnd: Infinity
};


================================================
FILE: .stylelintignore
================================================
/dist/*
/public/*
public/*
stats.html


================================================
FILE: .stylelintrc.cjs
================================================
// @see: https://stylelint.io

module.exports = {
  root: true,
  // 继承某些已有的规则
  extends: [
    "stylelint-config-standard", // 配置 stylelint 拓展插件
    "stylelint-config-html/vue", // 配置 vue 中 template 样式格式化
    "stylelint-config-standard-scss", // 配置 stylelint scss 插件
    "stylelint-config-recommended-vue/scss", // 配置 vue 中 scss 样式格式化
    "stylelint-config-recess-order" // 配置 stylelint css 属性书写顺序插件,
  ],
  overrides: [
    // 扫描 .vue/html 文件中的 <style> 标签内的样式
    {
      files: ["**/*.{vue,html}"],
      customSyntax: "postcss-html"
    }
  ],
  rules: {
    "function-url-quotes": "always", // URL 的引号 "always(必须加上引号)"|"never(没有引号)"
    "color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简写)"|"long(16进制扩写)"
    "rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有一个空行)"|"never-multi-line(多行规则之前绝不能有空行)"
    "font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表中缺少通用字体族关键字
    "scss/at-import-partial-extension": null, // 解决不能使用 @import 引入 scss 文件
    "property-no-unknown": null, // 禁止未知的属性
    "no-empty-source": null, // 禁止空源码
    "selector-class-pattern": null, // 强制选择器类名的格式
    "value-no-vendor-prefix": null, // 关闭 vendor-prefix (为了解决多行省略 -webkit-box)
    "no-descending-specificity": null, // 不允许较低特异性的选择器出现在覆盖较高特异性的选择器
    "value-keyword-case": null, // 解决在 scss 中使用 v-bind 大写单词报错
    "selector-pseudo-class-no-unknown": [
      true,
      {
        ignorePseudoClasses: ["global", "v-deep", "deep"]
      }
    ]
  },
  ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts"]
};


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "vue.volar",
    "hollowtree.vue-snippets",
    "dbaeumer.vscode-eslint",
    "stylelint.vscode-stylelint",
    "esbenp.prettier-vscode",
    "editorconfig.editorconfig",
    "streetsidesoftware.code-spell-checker",
    "syler.sass-indented",
    "mikestead.dotenv"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit"
  },
  "stylelint.enable": true,
  "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass", "html"],
  "files.eol": "\n",
  "typescript.tsdk": "node_modules/typescript/lib",
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[less]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "cSpell.words": [
    "AMAP",
    "apng",
    "axios",
    "Biao",
    "brotli",
    "cascader",
    "commitlint",
    "contentleft",
    "contentright",
    "CSDN",
    "daterange",
    "datetimerange",
    "echarts",
    "fangda",
    "geeker",
    "Gitee",
    "hexs",
    "huiche",
    "iconfont",
    "juejin",
    "liquidfill",
    "longpress",
    "monthrange",
    "nprogress",
    "officedocument",
    "openxmlformats",
    "Pageable",
    "persistedstate",
    "pinia",
    "pjpeg",
    "Prefixs",
    "screenfull",
    "sortablejs",
    "sousuo",
    "spreadsheetml",
    "styl",
    "stylelint",
    "stylelintignore",
    "stylelintrc",
    "suoxiao",
    "truetype",
    "tuichu",
    "unplugin",
    "unref",
    "VITE",
    "vuedraggable",
    "vueuse",
    "Vuex",
    "wangeditor",
    "xiala",
    "xiaoxi",
    "Yahei",
    "yiwen",
    "zhongyingwen",
    "zhuti"
  ]
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [1.2.0](https://github.com/HalseySpicy/Geeker-Admin/compare/v1.1.0...v1.2.0) (2023-09-15)


### Features

* 🚀 update and optimize project content ([17bc017](https://github.com/HalseySpicy/Geeker-Admin/commit/17bc017c5abbd2c87813d6c6f9d587ddf9d57da7))
* 🚀 upgrade plugins and add tab functionality ([f21a41d](https://github.com/HalseySpicy/Geeker-Admin/commit/f21a41d8df44efe5216dec39bf4abf0ea86a7781))

## [1.1.0](https://github.com/HalseySpicy/Geeker-Admin/compare/v1.0.0...v1.1.0) (2023-07-09)

### Features

- 🚀 add proTable instance type ([8262f04](https://github.com/HalseySpicy/Geeker-Admin/commit/8262f045734d055148720738a80fee0e0c779ceb))
- 🚀 optimize code and add VitePWA ([523f676](https://github.com/HalseySpicy/Geeker-Admin/commit/523f676feee5105eae15d05b57063227be26d3df))
- 🚀 optimize code and update plugins ([5cff3c7](https://github.com/HalseySpicy/Geeker-Admin/commit/5cff3c7e50331ede07d57613cc49658904e7cf1a))
- 🚀 optimize irregular code ([d406ef2](https://github.com/HalseySpicy/Geeker-Admin/commit/d406ef2bb5e0d0522f5e0ce38c3b9d0aa47c7cd2))
- 🚀 proTable radio selection example ([f58291f](https://github.com/HalseySpicy/Geeker-Admin/commit/f58291f96468b816607c31df87326ebd7d0a5c5a))
- 🚀 proTable search component custom rendering ([#189](https://github.com/HalseySpicy/Geeker-Admin/issues/189)) ([89f03db](https://github.com/HalseySpicy/Geeker-Admin/commit/89f03db2db41160b9ac5398d64712fabff399c4c))
- 🚀 proTable supports static table data ([9a3e85d](https://github.com/HalseySpicy/Geeker-Admin/commit/9a3e85d21d6c8872bbc915c42d48eb8658a1db63))
- 🚀 update Gitee address ([0608be9](https://github.com/HalseySpicy/Geeker-Admin/commit/0608be9ba6a9f01f73ed67671d340cf0c1d99261))
- 🚀 update stylelint configuration ([d62aae8](https://github.com/HalseySpicy/Geeker-Admin/commit/d62aae868e7d6aad87f0ba8706ab9744d5da04dd))
- 🚀 update theme, modify bugs ([7a3a7a3](https://github.com/HalseySpicy/Geeker-Admin/commit/7a3a7a3d665c0356c0272947292de750dd0be8d8))

### Bug Fixes

- 🧩 fix eslint error ([a980a1a](https://github.com/HalseySpicy/Geeker-Admin/commit/a980a1aa7808895af46c85e0e26a53f66d1119fa))

## [1.0.0](https://github.com/HalseySpicy/Geeker-Admin/compare/v0.0.7...v1.0.0) (2023-04-15)

### Features

- 🚀 升级依赖插件 && 新增树型 ProTbale 示例(更多查看详情) ([ed0ea75](https://github.com/HalseySpicy/Geeker-Admin/commit/ed0ea757555f047f6890632e598ee3293d3598cd))
- 🚀 升级依赖插件 && 修复 bug(查看详情) ([4febadc](https://github.com/HalseySpicy/Geeker-Admin/commit/4febadc10dd794ec8ea1c7864a3771b1b477f743))
- 🚀 新增路由白名单访问控制 ([97dc264](https://github.com/HalseySpicy/Geeker-Admin/commit/97dc26484c6eead2ae4c8c79d50b550f24f19a02))
- 🚀 优化 ProTable && 面包屑导航 ([905d7f1](https://github.com/HalseySpicy/Geeker-Admin/commit/905d7f1fd2b18d9650e6ba7d439dfdcf50363d11))
- 🚀 优化代码 ([cd333df](https://github.com/HalseySpicy/Geeker-Admin/commit/cd333dfe5de2aa7fa415326e6a06b83d3bd260d5))
- 🚀 优化代码和样式细节 ([5b4b926](https://github.com/HalseySpicy/Geeker-Admin/commit/5b4b9266de4f420f32fca70dadb76242d129e604))
- 🚀 优化代码和样式细节 ([756094c](https://github.com/HalseySpicy/Geeker-Admin/commit/756094c402e14841c07cd6062b701929f7f31737))
- 🚀 优化代码逻辑 && 更新微信群二维码 ([629e824](https://github.com/HalseySpicy/Geeker-Admin/commit/629e8243466fda5da9f0ec781aa0b584e49f4501))
- 🚀 优化代码细节问题 ([a6a6ced](https://github.com/HalseySpicy/Geeker-Admin/commit/a6a6cedeb40f2f7901f0dcd0ec7f1c283a491c61))
- 🚀 优化样式、代码细节 ([1b02f45](https://github.com/HalseySpicy/Geeker-Admin/commit/1b02f457162267b090ad946e0bad91e5d0dd14b1))
- 🚀 allow nested tree enum data ([c2fa2be](https://github.com/HalseySpicy/Geeker-Admin/commit/c2fa2be54a6af0309ba45bd4ca68170c66edc357))
- 🚀 refactoring project configuration ([7ede988](https://github.com/HalseySpicy/Geeker-Admin/commit/7ede988bae3ad0b33d9e5ac1ea6145c4d7aa89e6))
- **ProTable:** 🚀 插槽引入 ElTable 的 scope,可获取$index 等 ([4cb7dba](https://github.com/HalseySpicy/Geeker-Admin/commit/4cb7dba40c10e693e324b7c647aa65917aeb0b02))

### Bug Fixes

- 🧩 修复 ImportExcel 组件 bug ([ab7e9dd](https://github.com/HalseySpicy/Geeker-Admin/commit/ab7e9dde400aa80ec2e9fa58d9f2168fc3d14f18))
- 🧩 修复 ImportExcel 组件 bug ([803ba58](https://github.com/HalseySpicy/Geeker-Admin/commit/803ba58a2c3fae7f6d8783ca534e2b41c987f027))
- 🧩 修复 ProTable 组件打印功能 bug ([a88b7df](https://github.com/HalseySpicy/Geeker-Admin/commit/a88b7df4623e30459ef3c92196b720efcb200f2f))
- 🧩 修复 TreeFilter 组件默认值 bug ([8e515f0](https://github.com/HalseySpicy/Geeker-Admin/commit/8e515f0d4058f573cbd53281ef68aec38b8dacb9))
- 🧩 修复 TreeFilter 组件默认值 bug ([f23a94d](https://github.com/HalseySpicy/Geeker-Admin/commit/f23a94d6edf442babdc4cd5a52ea63ebbbcac44f))
- 🧩 修复 useDebounceFn 错误使用 ([99d4278](https://github.com/HalseySpicy/Geeker-Admin/commit/99d4278a29b7fa970caba55f43134cebd1d3bec6))
- 🧩 修复多图片上传预览初始化异常 ([d1a917f](https://github.com/HalseySpicy/Geeker-Admin/commit/d1a917f10326b7742b4445c495de12603df658c1))
- 🧩 修复分栏布局路径匹配 bug ([b06bd12](https://github.com/HalseySpicy/Geeker-Admin/commit/b06bd123fa6af0dcb7d5cc9bc9215f13b91ace5f))
- 🧩 修复横向布局下最大化失效 ([e416ddb](https://github.com/HalseySpicy/Geeker-Admin/commit/e416ddb732ae336ca4e7e43ebdeb89b24085d1b1))
- 🧩 修复路由重置 bug ([52b7e66](https://github.com/HalseySpicy/Geeker-Admin/commit/52b7e66febf1db9cd3325eb0e1e45213ff1528c2))
- 🧩 修复弱类型检查错误 ([b32310e](https://github.com/HalseySpicy/Geeker-Admin/commit/b32310ed2f546e3efa918d5f7eee2c3868a98cb7))

### 0.0.7 (2022-12-28)

### Features

- 🚀 二次封装 wangEditor 富文本编辑器(50%) ([4f8e266](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4f8e266b7dd25a7df18d302e88e14454bfa3816b))
- 🚀 更新插件、优化代码(请查看详情) ([dac6dec](https://gitee.com/HalseySpicy/Geeker-Admin/commit/dac6dec75466c19731ad7cf083f8c39940342140))
- 🚀 更新微信群二维码 ([7e890d0](https://gitee.com/HalseySpicy/Geeker-Admin/commit/7e890d0afe0a11170d73e3c2c4ef04d37a582e94))
- 🚀 请求全局 loading 更改为可配置 ([a75d62f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a75d62f627195ac420cf24ad7f51245b2e5bf04e))
- 🚀 升级 element-plus 到 2.25 ([e98c035](https://gitee.com/HalseySpicy/Geeker-Admin/commit/e98c035caa6d1ab04319673e0db65837c6887126))
- 🚀 升级 vite、vue 版本 && 优化分栏布局样式 ([b2b1b59](https://gitee.com/HalseySpicy/Geeker-Admin/commit/b2b1b599bc1fa0f1c64c5c58fb31d3719f415301))
- 🚀 使用属性透传重构 ProTable 组件 ([a428e89](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a428e89a3784c826eceaaee548b97975afbe1d45))
- 🚀 添加 wangEditor 组件 ([d6d2fa7](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d6d2fa7d27887bb4a9e40e9d7037d4621812e16a))
- 🚀 完成 wangEditor 富文本二次封装 ([7362bfb](https://gitee.com/HalseySpicy/Geeker-Admin/commit/7362bfbff19224045e3bb20fa939a78c556cc805))
- 🚀 完善按钮、菜单权限示例 ([6793f0c](https://gitee.com/HalseySpicy/Geeker-Admin/commit/6793f0cd7372b8a080f6d2649b05cdd0c62bd853))
- 🚀 新增 主题色、灰色模式、色弱模式 配置 ([7821157](https://gitee.com/HalseySpicy/Geeker-Admin/commit/7821157059ed9c21d2844f75049f8fa999b19944))
- 🚀 新增 pro-form ([3ab5a5b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3ab5a5b4f63fca227944ab6cc7928f6bf1f88ed4))
- 🚀 新增 protable 打印、列对齐方式功能 ([c22879e](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c22879e7e80ff9ef662c39daa25b11f5f17d17ca))
- 🚀 新增 protbale 功能, 请查看详情 ([17f2bcd](https://gitee.com/HalseySpicy/Geeker-Admin/commit/17f2bcd67362365579ed8a572a3a9d17368ac64e))
- 🚀 新增 SVG Icons ([977602c](https://gitee.com/HalseySpicy/Geeker-Admin/commit/977602c30b8997cb51426fe9498392edc249561d))
- 🚀 新增 treeFilter 组件标题属性 ([20c755f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/20c755f59f3ae2b0380e6549bb56bb22317d750e))
- 🚀 新增 treeFilter data 参数 ([4280766](https://gitee.com/HalseySpicy/Geeker-Admin/commit/428076635d7a0e9f80109274d9523cf91aa5a10c))
- 🚀 新增暗黑模式 ([215e499](https://gitee.com/HalseySpicy/Geeker-Admin/commit/215e499634b516234e653eac27a611d5f51ea6da))
- 🚀 新增菜单搜索功能 ([4aa0eef](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4aa0eefaf427a2aa1aebd2b78dc049ffa776e838))
- 🚀 新增动态路由 ([551fefc](https://gitee.com/HalseySpicy/Geeker-Admin/commit/551fefc2e66b067d9e64d3b0cfbf47dfa1057d98))
- 🚀 新增分栏布局 ([de37143](https://gitee.com/HalseySpicy/Geeker-Admin/commit/de37143e93c0cc5be2ff52466dce344ab9270f0d))
- 🚀 新增功能 && 修复 bug(查看详情) ([1ab183f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/1ab183f1551cb8beb77243c2953b0119409dd6a5))
- 🚀 新增功能 && 修复 bug(查看详情) ([4c0bc5f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4c0bc5fd3c111e1cac636cad104c83ffb1168679))
- 🚀 新增功能(查看详情) ([cbd8dc2](https://gitee.com/HalseySpicy/Geeker-Admin/commit/cbd8dc2387576f525c0e49f81d540fbad3cb5e81))
- 🚀 新增横向、纵向、经典布局切换 ([1046de4](https://gitee.com/HalseySpicy/Geeker-Admin/commit/1046de4c7d5f805b10c5cea5325b063e3d6dd84f))
- 🚀 新增界面配置功能 ([39ffc5e](https://gitee.com/HalseySpicy/Geeker-Admin/commit/39ffc5e9a77da3294055f23f8c87a4a44f3622f7))
- 🚀 新增路由相关功能 ([9679eed](https://gitee.com/HalseySpicy/Geeker-Admin/commit/9679eed1edd0c1f08c17465f590d4ca0365985ee)), closes [#71](https://gitee.com/HalseySpicy/Geeker-Admin/issues/71) [#72](https://gitee.com/HalseySpicy/Geeker-Admin/issues/72) [#49](https://gitee.com/HalseySpicy/Geeker-Admin/issues/49)
- 🚀 新增请求示例,参见 loginApi ([d49b227](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d49b227762ae48c3ca08f0dec02a3667daac8532))
- 🚀 新增图标选择组件 ([ce5e165](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ce5e165aed842074a9f7ac66ea97290710b541ee))
- 🚀 新增图片上传组件 ([c50c421](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c50c421bc3c5f7af68184cda88262c6fb1bd07e0))
- 🚀 新增图片上传组件属性 ([d7670ed](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d7670ed94608c5410f3102d7b9427d8d856204b1))
- 🚀 新增系统管理模块 ([23748e1](https://gitee.com/HalseySpicy/Geeker-Admin/commit/23748e185e80e3b774b42114427934228a57d3aa))
- 🚀 新增消息通知 ([66836b6](https://gitee.com/HalseySpicy/Geeker-Admin/commit/66836b69781ccc55402a3887d091149885864442))
- 🚀 新增页面刷新功能 ([5223a41](https://gitee.com/HalseySpicy/Geeker-Admin/commit/5223a416d17568d5b2cae7b16b637e0f39134223))
- 🚀 新增引导页 ([4fb6fb3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4fb6fb3a3eb34f82576e2378c311ff580f65226d))
- 🚀 新增组件参数配置文档 ([0e11fc5](https://gitee.com/HalseySpicy/Geeker-Admin/commit/0e11fc59175d5d74730c3cb1fa2579effcca6e48))
- 🚀 修改 keepAlive 逻辑 ([168ca13](https://gitee.com/HalseySpicy/Geeker-Admin/commit/168ca13e796c8cc366caa3d6e05090acdaefef75))
- 🚀 修改 pinia 持久化插件 ([a7691ae](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a7691aea614a035c4d381838149e08ad8477e49f))
- 🚀 优化代码注释 && 升级 element 到 2.2.6 ([b84512b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/b84512b3b102b00faa2f9241a32f5fbe27da4307))
- 🚀 优化注释 && 代码细节问题 ([9d0ffa5](https://gitee.com/HalseySpicy/Geeker-Admin/commit/9d0ffa5ddecc4c73bec51208b05a6d44b1523b1f))
- 🚀 预定义主题颜色 ([8219178](https://gitee.com/HalseySpicy/Geeker-Admin/commit/82191789bcf6d21c623aa61c5a64e502cea44c2c))
- 🚀 增加 SearchForm 属性透传 ([eadb89b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/eadb89b687596980a82401f44c53430081078d04))
- 🚀 增加表格 treeFilter、更新整体布局样式 ([719b78f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/719b78f317589b983bc4b852b3bfd63a60d42a46))
- 🚀 增加布局方式切换,样式已完成 ([5745b93](https://gitee.com/HalseySpicy/Geeker-Admin/commit/5745b93a6cc00519c1a02977b8c0437502d867e6))
- 🚀 增加分类筛选器 ([c95a1c0](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c95a1c054ee9eacae470bcaae7574d5c989b86a2))
- 🚀 增加全局错误拦截 && 修改细节问题 请查看详情 ([0496184](https://gitee.com/HalseySpicy/Geeker-Admin/commit/04961847eb7df004d1e9f562e78ea3d5f851ea49))

### Bug Fixes

- 🧩 菜单搜索过滤掉 isHide 为 true 的菜单 ([c6bab35](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c6bab356f0cde7e3dc6f69dfac115239c2453776))
- 🧩 解决 useTable 查询参数 bug ([a86e408](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a86e4089b6da8ab6a55bc84e069d665c06471676))
- 🧩 去除登陆页默认账号 ([3dda3fe](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3dda3fee3fef38fdafcfdf3b1bf16e73033c6fe0))
- 🧩 删除 protable 组件 image 配置属性 ([d699fe7](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d699fe7bd55eaaccfad9b94105c1b43ae64d1c34))
- 🧩 修复 国际化 产生的 bug ([ec4f74a](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ec4f74ae654e7287fc08bb31fa3ee3d2c76164eb))
- 🧩 修复 axios 请求超时未拦截错误 ([856468e](https://gitee.com/HalseySpicy/Geeker-Admin/commit/856468e84f8356d35c25097f3115dfe3d496914c))
- 🧩 修复 bug ([3714abd](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3714abdc4826034791ccb3fc8249d946ec3a4e16))
- 🧩 修复 Pro-Tabel 列设置 bug ([a3b86a0](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a3b86a06a6d9cd4b6f7ac6e108727a0b4852e9a0))
- 🧩 修复 pro-table 格式报错问题 ([2ef11fd](https://gitee.com/HalseySpicy/Geeker-Admin/commit/2ef11fda6d373c3214df801ae789cafc1a033dcb))
- 🧩 修复布局样式 bug ([2f1cd64](https://gitee.com/HalseySpicy/Geeker-Admin/commit/2f1cd6442f359909301e3d95b0ed4dc9d2dbe7c6))
- 🧩 修复打包错误 ([243ebfc](https://gitee.com/HalseySpicy/Geeker-Admin/commit/243ebfc5280ddc013056c6708b44df35fe18f613))
- 🧩 修复打包失败 ([31698fe](https://gitee.com/HalseySpicy/Geeker-Admin/commit/31698fea6478d60343a9ad49ae0fc6db7a42c184))
- 🧩 修复打包失败问题 ([1778651](https://gitee.com/HalseySpicy/Geeker-Admin/commit/1778651781a1bb8bfe4ea61dafb9b48773fef5d7))
- 🧩 修复分栏布局 bug ([113274a](https://gitee.com/HalseySpicy/Geeker-Admin/commit/113274a87e2dacf694648f3a304c7ac37e2262d0))
- 🧩 修复经典布局展示 bug ([b95e237](https://gitee.com/HalseySpicy/Geeker-Admin/commit/b95e2376d06c6a6a35f72743e3fe8c1569fda008))
- 🧩 修复路由跳转两次不能携带参数问题 ([8b583f3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/8b583f3d5f05b77ec2a35082557bae431441a586))
- 🧩 修复请求 header 参数丢失 bug ([3598dbc](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3598dbc2a83aaacf9dada4e2c38a3ca27cbe4cfd))
- 🧩 修复上传组件细节问题 ([8528358](https://gitee.com/HalseySpicy/Geeker-Admin/commit/8528358925ea809cf52f55015355345e87607351))
- 🧩 修复 BUG ([4bf2988](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4bf29881dd41fad256f1beb5affcd5ba6599e17d))
- 🧩 修复 BUG ([c93aaf7](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c93aaf700112decd158e9a5a9c1f83eff1773e91))
- 🧩 修复 loading 请求 bug ([a3270ec](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a3270ecfa2e7c2484729ae6fd599febcc4f7be6b))
- 🧩 修复 vercel 打包失败 ([e63dee1](https://gitee.com/HalseySpicy/Geeker-Admin/commit/e63dee1f9653f4f95d0330275c5f5e8b530564c9))
- 🧩 修改 Pro-Table 表头渲染方式 ([aa57294](https://gitee.com/HalseySpicy/Geeker-Admin/commit/aa5729489942eaa6dca9928b70153af2de753a9c))
- 🧩 修改 useTable 存在的 bug ([5bb55b3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/5bb55b32c0b46bbf55fa0d49efe3a15d0b1673a4))
- 🧩 修改 useTable 钩子中的 bug ([675aed8](https://gitee.com/HalseySpicy/Geeker-Admin/commit/675aed806e62c236b40bc933402c86085289df4e))
- 🧩 修改 useTable 携带默认查询参数 bug ([ee585b2](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ee585b29f3129b7143a10947fdd3184b197ad883))
- 🧩 修改代码细节 && 优化注释 ([d86cb1f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d86cb1feb32e11a29e1c2bee54ea788c6c828d75))
- 🧩 修改当菜单设置 isHide=true 时面包屑报错 ([66885c5](https://gitee.com/HalseySpicy/Geeker-Admin/commit/66885c5cc15c10ccadcd49a7bee27a821663e8a7))
- 🧩 修改文件导出失败 bug ([208e720](https://gitee.com/HalseySpicy/Geeker-Admin/commit/208e720688969d2bc0fa0a6cc2bae3e3b991c806))
- 🧩 修改 BUG ([540048a](https://gitee.com/HalseySpicy/Geeker-Admin/commit/540048a09be9b0df5443e275f38f43c80dcde51f))
- 🧩 fix use pinia bug ([609aa69](https://gitee.com/HalseySpicy/Geeker-Admin/commit/609aa69aa9b3e0bb4e667ee7f76ab44051c2d2e8))
- 修复登录后白屏 ([f986c5c](https://gitee.com/HalseySpicy/Geeker-Admin/commit/f986c5c44fc1df8d5c6a90e90239c06928e2f4a1))
- **el-table:** 🧩 修复 el-table 在 safari 浏览器错乱 ([b776a48](https://gitee.com/HalseySpicy/Geeker-Admin/commit/b776a483636547c7cee723846ec33b2842550d13))

### 0.0.6 (2022-08-22)

### Features

- 🚀 二次封装 wangEditor 富文本编辑器(50%) ([4f8e266](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4f8e266b7dd25a7df18d302e88e14454bfa3816b))
- 🚀 请求全局 loading 更改为可配置 ([a75d62f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a75d62f627195ac420cf24ad7f51245b2e5bf04e))
- 🚀 升级 element-plus 到 2.25 ([e98c035](https://gitee.com/HalseySpicy/Geeker-Admin/commit/e98c035caa6d1ab04319673e0db65837c6887126))
- 🚀 添加 wangEditor 组件 ([d6d2fa7](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d6d2fa7d27887bb4a9e40e9d7037d4621812e16a))
- 🚀 完成 wangEditor 富文本二次封装 ([7362bfb](https://gitee.com/HalseySpicy/Geeker-Admin/commit/7362bfbff19224045e3bb20fa939a78c556cc805))
- 🚀 新增 主题色、灰色模式、色弱模式 配置 ([7821157](https://gitee.com/HalseySpicy/Geeker-Admin/commit/7821157059ed9c21d2844f75049f8fa999b19944))
- 🚀 新增 pro-form ([3ab5a5b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3ab5a5b4f63fca227944ab6cc7928f6bf1f88ed4))
- 🚀 新增 protbale 功能, 请查看详情 ([17f2bcd](https://gitee.com/HalseySpicy/Geeker-Admin/commit/17f2bcd67362365579ed8a572a3a9d17368ac64e))
- 🚀 新增 SVG Icons ([977602c](https://gitee.com/HalseySpicy/Geeker-Admin/commit/977602c30b8997cb51426fe9498392edc249561d))
- 🚀 新增 treeFilter data 参数 ([4280766](https://gitee.com/HalseySpicy/Geeker-Admin/commit/428076635d7a0e9f80109274d9523cf91aa5a10c))
- 🚀 新增暗黑模式 ([215e499](https://gitee.com/HalseySpicy/Geeker-Admin/commit/215e499634b516234e653eac27a611d5f51ea6da))
- 🚀 新增菜单搜索功能 ([4aa0eef](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4aa0eefaf427a2aa1aebd2b78dc049ffa776e838))
- 🚀 新增界面配置功能 ([39ffc5e](https://gitee.com/HalseySpicy/Geeker-Admin/commit/39ffc5e9a77da3294055f23f8c87a4a44f3622f7))
- 🚀 新增请求示例,参见 loginApi ([d49b227](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d49b227762ae48c3ca08f0dec02a3667daac8532))
- 🚀 新增图标选择组件 ([ce5e165](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ce5e165aed842074a9f7ac66ea97290710b541ee))
- 🚀 新增图片上传组件 ([c50c421](https://gitee.com/HalseySpicy/Geeker-Admin/commit/c50c421bc3c5f7af68184cda88262c6fb1bd07e0))
- 🚀 新增图片上传组件属性 ([d7670ed](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d7670ed94608c5410f3102d7b9427d8d856204b1))
- 🚀 新增引导页 ([4fb6fb3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/4fb6fb3a3eb34f82576e2378c311ff580f65226d))
- 🚀 新增组件参数配置文档 ([0e11fc5](https://gitee.com/HalseySpicy/Geeker-Admin/commit/0e11fc59175d5d74730c3cb1fa2579effcca6e48))
- 🚀 修改 pinia 持久化插件 ([a7691ae](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a7691aea614a035c4d381838149e08ad8477e49f))
- 🚀 优化代码注释 && 升级 element 到 2.2.6 ([b84512b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/b84512b3b102b00faa2f9241a32f5fbe27da4307))
- 🚀 优化注释 && 代码细节问题 ([9d0ffa5](https://gitee.com/HalseySpicy/Geeker-Admin/commit/9d0ffa5ddecc4c73bec51208b05a6d44b1523b1f))
- 🚀 预定义主题颜色 ([8219178](https://gitee.com/HalseySpicy/Geeker-Admin/commit/82191789bcf6d21c623aa61c5a64e502cea44c2c))
- 🚀 增加 SearchForm 属性透传 ([eadb89b](https://gitee.com/HalseySpicy/Geeker-Admin/commit/eadb89b687596980a82401f44c53430081078d04))
- 🚀 增加表格 treeFilter、更新整体布局样式 ([719b78f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/719b78f317589b983bc4b852b3bfd63a60d42a46))

### Bug Fixes

- 🧩 解决 useTable 查询参数 bug ([a86e408](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a86e4089b6da8ab6a55bc84e069d665c06471676))
- 🧩 去除登陆页默认账号 ([3dda3fe](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3dda3fee3fef38fdafcfdf3b1bf16e73033c6fe0))
- 🧩 删除 protable 组件 image 配置属性 ([d699fe7](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d699fe7bd55eaaccfad9b94105c1b43ae64d1c34))
- 🧩 修复 国际化 产生的 bug ([ec4f74a](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ec4f74ae654e7287fc08bb31fa3ee3d2c76164eb))
- 🧩 修复 axios 请求超时未拦截错误 ([856468e](https://gitee.com/HalseySpicy/Geeker-Admin/commit/856468e84f8356d35c25097f3115dfe3d496914c))
- 🧩 修复 Pro-Tabel 列设置 bug ([a3b86a0](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a3b86a06a6d9cd4b6f7ac6e108727a0b4852e9a0))
- 🧩 修复 pro-table 格式报错问题 ([2ef11fd](https://gitee.com/HalseySpicy/Geeker-Admin/commit/2ef11fda6d373c3214df801ae789cafc1a033dcb))
- 🧩 修复打包失败问题 ([1778651](https://gitee.com/HalseySpicy/Geeker-Admin/commit/1778651781a1bb8bfe4ea61dafb9b48773fef5d7))
- 🧩 修复路由跳转两次不能携带参数问题 ([8b583f3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/8b583f3d5f05b77ec2a35082557bae431441a586))
- 🧩 修复请求 header 参数丢失 bug ([3598dbc](https://gitee.com/HalseySpicy/Geeker-Admin/commit/3598dbc2a83aaacf9dada4e2c38a3ca27cbe4cfd))
- 🧩 修复上传组件细节问题 ([8528358](https://gitee.com/HalseySpicy/Geeker-Admin/commit/8528358925ea809cf52f55015355345e87607351))
- 🧩 修复 loading 请求 bug ([a3270ec](https://gitee.com/HalseySpicy/Geeker-Admin/commit/a3270ecfa2e7c2484729ae6fd599febcc4f7be6b))
- 🧩 修改 Pro-Table 表头渲染方式 ([aa57294](https://gitee.com/HalseySpicy/Geeker-Admin/commit/aa5729489942eaa6dca9928b70153af2de753a9c))
- 🧩 修改 useTable 存在的 bug ([5bb55b3](https://gitee.com/HalseySpicy/Geeker-Admin/commit/5bb55b32c0b46bbf55fa0d49efe3a15d0b1673a4))
- 🧩 修改 useTable 钩子中的 bug ([675aed8](https://gitee.com/HalseySpicy/Geeker-Admin/commit/675aed806e62c236b40bc933402c86085289df4e))
- 🧩 修改 useTable 携带默认查询参数 bug ([ee585b2](https://gitee.com/HalseySpicy/Geeker-Admin/commit/ee585b29f3129b7143a10947fdd3184b197ad883))
- 🧩 修改代码细节 && 优化注释 ([d86cb1f](https://gitee.com/HalseySpicy/Geeker-Admin/commit/d86cb1feb32e11a29e1c2bee54ea788c6c828d75))
- 🧩 修改文件导出失败 bug ([208e720](https://gitee.com/HalseySpicy/Geeker-Admin/commit/208e720688969d2bc0fa0a6cc2bae3e3b991c806))
- 🧩 fix use pinia bug ([609aa69](https://gitee.com/HalseySpicy/Geeker-Admin/commit/609aa69aa9b3e0bb4e667ee7f76ab44051c2d2e8))
- 修复登录后白屏 ([f986c5c](https://gitee.com/HalseySpicy/Geeker-Admin/commit/f986c5c44fc1df8d5c6a90e90239c06928e2f4a1))

### [0.0.5](https://github.com/HalseySpicy/Geeker-Admin/compare/v0.0.4...v0.0.5) (2022-07-21)

### Features

- 🚀 新增请求示例,参见 loginApi ([d49b227](https://github.com/HalseySpicy/Geeker-Admin/commit/d49b227762ae48c3ca08f0dec02a3667daac8532))

### Bug Fixes

- 🧩 解决 useTable 查询参数 bug ([a86e408](https://github.com/HalseySpicy/Geeker-Admin/commit/a86e4089b6da8ab6a55bc84e069d665c06471676))
- 🧩 修复 axios 请求超时未拦截错误 ([856468e](https://github.com/HalseySpicy/Geeker-Admin/commit/856468e84f8356d35c25097f3115dfe3d496914c))
- 🧩 修复请求 header 参数丢失 bug ([3598dbc](https://github.com/HalseySpicy/Geeker-Admin/commit/3598dbc2a83aaacf9dada4e2c38a3ca27cbe4cfd))

### [0.0.4](https://github.com/HalseySpicy/Geeker-Admin/compare/v0.0.3...v0.0.4) (2022-07-12)

### Features

- 🚀 新增 主题色、灰色模式、色弱模式 配置 ([7821157](https://github.com/HalseySpicy/Geeker-Admin/commit/7821157059ed9c21d2844f75049f8fa999b19944))
- 🚀 新增 pro-form ([3ab5a5b](https://github.com/HalseySpicy/Geeker-Admin/commit/3ab5a5b4f63fca227944ab6cc7928f6bf1f88ed4))
- 🚀 新增菜单搜索功能 ([4aa0eef](https://github.com/HalseySpicy/Geeker-Admin/commit/4aa0eefaf427a2aa1aebd2b78dc049ffa776e838))
- 🚀 新增界面配置功能 ([39ffc5e](https://github.com/HalseySpicy/Geeker-Admin/commit/39ffc5e9a77da3294055f23f8c87a4a44f3622f7))
- 🚀 预定义主题颜色 ([8219178](https://github.com/HalseySpicy/Geeker-Admin/commit/82191789bcf6d21c623aa61c5a64e502cea44c2c))
- 🚀 增加 SearchForm 属性透传 ([eadb89b](https://github.com/HalseySpicy/Geeker-Admin/commit/eadb89b687596980a82401f44c53430081078d04))

### Bug Fixes

- 🧩 修复 pro-table 格式报错问题 ([2ef11fd](https://github.com/HalseySpicy/Geeker-Admin/commit/2ef11fda6d373c3214df801ae789cafc1a033dcb))
- 🧩 修改文件导出失败 bug ([208e720](https://github.com/HalseySpicy/Geeker-Admin/commit/208e720688969d2bc0fa0a6cc2bae3e3b991c806))
- 🧩 fix use pinia bug ([609aa69](https://github.com/HalseySpicy/Geeker-Admin/commit/609aa69aa9b3e0bb4e667ee7f76ab44051c2d2e8))

### [0.0.2](https://github.com/HalseySpicy/Geeker-Admin/compare/v0.0.3...v0.0.2) (2022-06-29)

### 0.0.2 (2022-06-20)

### Features

- 🚀 请求全局 loading 更改为可配置 ([a75d62f](https://github.com/HalseySpicy/Geeker-Admin/commit/a75d62f627195ac420cf24ad7f51245b2e5bf04e))
- 🚀 升级 element-plus 到 2.2.5 ([e98c035](https://github.com/HalseySpicy/Geeker-Admin/commit/e98c035caa6d1ab04319673e0db65837c6887126))
- 🚀 新增暗黑模式 ([215e499](https://github.com/HalseySpicy/Geeker-Admin/commit/215e499634b516234e653eac27a611d5f51ea6da))
- 🚀 新增图标选择组件 ([ce5e165](https://github.com/HalseySpicy/Geeker-Admin/commit/ce5e165aed842074a9f7ac66ea97290710b541ee))
- 🚀 修改 pinia 持久化插件 ([a7691ae](https://github.com/HalseySpicy/Geeker-Admin/commit/a7691aea614a035c4d381838149e08ad8477e49f))
- 🚀 优化代码注释 && 升级 element 到 2.2.6 ([b84512b](https://github.com/HalseySpicy/Geeker-Admin/commit/b84512b3b102b00faa2f9241a32f5fbe27da4307))

### Bug Fixes

- 🧩 去除登陆页默认账号 ([3dda3fe](https://github.com/HalseySpicy/Geeker-Admin/commit/3dda3fee3fef38fdafcfdf3b1bf16e73033c6fe0))
- 🧩 修复 Pro-Table 列设置 bug ([a3b86a0](https://github.com/HalseySpicy/Geeker-Admin/commit/a3b86a06a6d9cd4b6f7ac6e108727a0b4852e9a0))
- 🧩 修复 loading 请求 bug ([a3270ec](https://github.com/HalseySpicy/Geeker-Admin/commit/a3270ecfa2e7c2484729ae6fd599febcc4f7be6b))
- 🧩 修改 Pro-Table 表头渲染方式 ([aa57294](https://github.com/HalseySpicy/Geeker-Admin/commit/aa5729489942eaa6dca9928b70153af2de753a9c))
- 🧩 修改 useTable 存在的 bug ([5bb55b3](https://github.com/HalseySpicy/Geeker-Admin/commit/5bb55b32c0b46bbf55fa0d49efe3a15d0b1673a4))
- 🧩 修改 useTable 钩子中的 bug ([675aed8](https://github.com/HalseySpicy/Geeker-Admin/commit/675aed806e62c236b40bc933402c86085289df4e))
- 🧩 修改 useTable 携带默认查询参数 bug ([ee585b2](https://github.com/HalseySpicy/Geeker-Admin/commit/ee585b29f3129b7143a10947fdd3184b197ad883))


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Halsey

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Geeker-Admin

### 介绍 📖

Geeker-Admin 一款基于 Vue3.4、TypeScript、Vite5、Pinia、Element-Plus 开源的后台管理框架,使用目前最新技术栈开发。项目提供强大的 [ProTable](https://juejin.cn/post/7166068828202336263) 组件,在一定程度上提高您的开发效率。另外本项目还封装了一些常用组件、Hooks、指令、动态路由、按钮级别权限控制等功能。

### React 版本 🔥

- 有需要请加底部微信了解、购买

- Link:https://pro.spicyboy.cn

### 在线预览 👀

- Link:https://admin.spicyboy.cn

### 代码仓库 ⭐

- Gitee:https://gitee.com/HalseySpicy/Geeker-Admin
- GitHub:https://github.com/HalseySpicy/Geeker-Admin

### 项目文档 📚

- 项目更新日志:[CHANGELOG.md](./CHANGELOG.md)

- 项目文档地址:https://docs.spicyboy.cn

### 项目功能 🔨

- 使用 Vue3.4 + TypeScript 开发,单文件组件**<script setup>**
- 采用 Vite5 作为项目开发、打包工具(配置 gzip/brotli 打包、tsx 语法、跨域代理…)
- 使用 Pinia 替代 Vuex,轻量、简单、易用,集成 Pinia 持久化插件
- 使用 TypeScript 对 Axios 整个二次封装(请求拦截、取消、常用请求封装…)
- 基于 Element 二次封装 [ProTable](https://juejin.cn/post/7166068828202336263) 组件,表格页面全部为配置项 Columns
- 支持 Element 组件大小切换、多主题布局、暗黑模式、i18n 国际化
- 使用 VueRouter 配置动态路由权限拦截、路由懒加载,支持页面按钮权限控制
- 使用 KeepAlive 对页面进行缓存,支持多级嵌套路由缓存
- 常用自定义指令开发(权限、复制、水印、拖拽、节流、防抖、长按…)
- 使用 Prettier 统一格式化代码,集成 ESLint、Stylelint 代码校验规范
- 使用 husky、lint-staged、commitlint、czg、cz-git 规范提交信息

### 安装使用步骤 📔

- **Clone:**

```text
# Gitee
git clone https://gitee.com/HalseySpicy/Geeker-Admin.git
# GitHub
git clone https://github.com/HalseySpicy/Geeker-Admin.git
```

- **Install:**

```text
pnpm install
```

- **Run:**

```text
pnpm dev
pnpm serve
```

- **Build:**

```text
# 开发环境
pnpm build:dev

# 测试环境
pnpm build:test

# 生产环境
pnpm build:pro
```

- **Lint:**

```text
# eslint 检测代码
pnpm lint:eslint

# prettier 格式化代码
pnpm lint:prettier

# stylelint 格式化样式
pnpm lint:stylelint
```

- **commit:**

```text
# 提交代码(提交前会自动执行 lint:lint-staged 命令)
pnpm commit
```

### 项目截图 📷

- 登录页:

![login_light](https://i.imgtg.com/2023/04/13/8tknp.png)

![login_dark](https://i.imgtg.com/2023/04/13/8tmpP.png)

- 首页:

![home_light](https://i.imgtg.com/2023/04/13/8tl1j.png)

![home_dark](https://i.imgtg.com/2023/04/13/8tpfb.png)

- 表格页:

![table_light](https://i.imgtg.com/2023/04/13/8tfMx.png)

![table_dark](https://i.imgtg.com/2023/04/13/8tv8F.png)

- 数据可视化

![dashboard](https://i.imgtg.com/2023/04/14/82Grx.png)

- 数据大屏:

![dataScreen](https://i.imgtg.com/2023/01/16/QP8HF.png)

### 文件资源目录 📚

```text
Geeker-Admin
├─ .husky                  # husky 配置文件
├─ .vscode                 # VSCode 推荐配置
├─ build                   # Vite 配置项
├─ public                  # 静态资源文件(该文件夹不会被打包)
├─ src
│  ├─ api                  # API 接口管理
│  ├─ assets               # 静态资源文件
│  ├─ components           # 全局组件
│  ├─ config               # 全局配置项
│  ├─ directives           # 全局指令文件
│  ├─ enums                # 项目常用枚举
│  ├─ hooks                # 常用 Hooks 封装
│  ├─ languages            # 语言国际化 i18n
│  ├─ layouts              # 框架布局模块
│  ├─ routers              # 路由管理
│  ├─ stores               # pinia store
│  ├─ styles               # 全局样式文件
│  ├─ typings              # 全局 ts 声明
│  ├─ utils                # 常用工具库
│  ├─ views                # 项目所有页面
│  ├─ App.vue              # 项目主组件
│  ├─ main.ts              # 项目入口文件
│  └─ vite-env.d.ts        # 指定 ts 识别 vue
├─ .editorconfig           # 统一不同编辑器的编码风格
├─ .env                    # vite 常用配置
├─ .env.development        # 开发环境配置
├─ .env.production         # 生产环境配置
├─ .env.test               # 测试环境配置
├─ .eslintignore           # 忽略 Eslint 校验
├─ .eslintrc.cjs           # Eslint 校验配置文件
├─ .gitignore              # 忽略 git 提交
├─ .prettierignore         # 忽略 Prettier 格式化
├─ .prettierrc.cjs         # Prettier 格式化配置
├─ .stylelintignore        # 忽略 stylelint 格式化
├─ .stylelintrc.cjs        # stylelint 样式格式化配置
├─ CHANGELOG.md            # 项目更新日志
├─ commitlint.config.cjs   # git 提交规范配置
├─ index.html              # 入口 html
├─ LICENSE                 # 开源协议文件
├─ lint-staged.config.cjs  # lint-staged 配置文件
├─ package-lock.json       # 依赖包包版本锁
├─ package.json            # 依赖包管理
├─ postcss.config.cjs      # postcss 配置
├─ README.md               # README 介绍
├─ tsconfig.json           # typescript 全局配置
└─ vite.config.ts          # vite 全局配置文件
```

### 浏览器支持 🌎

- 本地开发推荐使用 Chrome 最新版浏览器 [Download](https://www.google.com/intl/zh-CN/chrome/)。
- 生产环境支持现代浏览器,不再支持 IE 浏览器,更多浏览器可以查看 [Can I Use Es Module](https://caniuse.com/?search=ESModule)。

| ![IE](https://i.imgtg.com/2023/04/11/8z7ot.png) | ![Edge](https://i.imgtg.com/2023/04/11/8zr3p.png) | ![Firefox](https://i.imgtg.com/2023/04/11/8zKiU.png) | ![Chrome](https://i.imgtg.com/2023/04/11/8zNrx.png) | ![Safari](https://i.imgtg.com/2023/04/11/8zeGj.png) |
| :---------------------------------------------: | :-----------------------------------------------: | :--------------------------------------------------: | :-------------------------------------------------: | :-------------------------------------------------: |
|                   not support                   |                  last 2 versions                  |                   last 2 versions                    |                   last 2 versions                   |                   last 2 versions                   |

### 项目后台接口 🧩

项目后台接口完全采用 Mock 数据,感谢以下 Mock 平台支持:

- FastMock: https://www.fastmock.site
- EasyMock:https://mock.mengxuegu.com

### 微信交流群 👨‍👨‍👦‍👦

微信一群、二群、三群、四群已满,加作者微信进入五群(支持知识付费)🤪

|                                               微信二维码                                                |
| :-----------------------------------------------------------------------------------------------------: |
| <img src="https://pic.ziyuan.wang/user/guest/2024/02/WX20240228-162952@2x_d164375fc0c16.png" width=170> |

### 捐赠 🍵

如果你正在使用这个项目或者喜欢这个项目的,可以通过以下方式支持我:

- Star、Fork、Watch 一键三连 🚀
- 通过微信、支付宝一次性捐款 ❤

|                                        微信                                        |                                       支付宝                                       |
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
| <img src="https://i.imgtg.com/2023/01/16/QRzBX.png" alt="Alipay QRcode" width=170> | <img src="https://i.imgtg.com/2023/01/16/QRFZt.png" alt="Wechat QRcode" width=170> |


================================================
FILE: build/getEnv.ts
================================================
import path from "path";

export function isDevFn(mode: string): boolean {
  return mode === "development";
}

export function isProdFn(mode: string): boolean {
  return mode === "production";
}

export function isTestFn(mode: string): boolean {
  return mode === "test";
}

/**
 * Whether to generate package preview
 */
export function isReportMode(): boolean {
  return process.env.VITE_REPORT === "true";
}

// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable): ViteEnv {
  const ret: any = {};

  for (const envName of Object.keys(envConf)) {
    let realName = envConf[envName].replace(/\\n/g, "\n");
    realName = realName === "true" ? true : realName === "false" ? false : realName;
    if (envName === "VITE_PORT") realName = Number(realName);
    if (envName === "VITE_PROXY") {
      try {
        realName = JSON.parse(realName);
      } catch (error) {}
    }
    ret[envName] = realName;
  }
  return ret;
}

/**
 * Get user root directory
 * @param dir file path
 */
export function getRootPath(...dir: string[]) {
  return path.resolve(process.cwd(), ...dir);
}


================================================
FILE: build/plugins.ts
================================================
import { resolve } from "path";
import { PluginOption } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { createHtmlPlugin } from "vite-plugin-html";
import { visualizer } from "rollup-plugin-visualizer";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import eslintPlugin from "vite-plugin-eslint";
import viteCompression from "vite-plugin-compression";
import vueSetupExtend from "unplugin-vue-setup-extend-plus/vite";
import NextDevTools from "vite-plugin-vue-devtools";
import { codeInspectorPlugin } from "code-inspector-plugin";

/**
 * 创建 vite 插件
 * @param viteEnv
 */
export const createVitePlugins = (viteEnv: ViteEnv): (PluginOption | PluginOption[])[] => {
  const { VITE_GLOB_APP_TITLE, VITE_REPORT, VITE_DEVTOOLS, VITE_PWA, VITE_CODEINSPECTOR } = viteEnv;
  return [
    vue(),
    // vue 可以使用 jsx/tsx 语法
    vueJsx(),
    // devTools
    VITE_DEVTOOLS && NextDevTools({ launchEditor: "code" }),
    // esLint 报错信息显示在浏览器界面上
    eslintPlugin(),
    // name 可以写在 script 标签上
    vueSetupExtend({}),
    // 创建打包压缩配置
    createCompression(viteEnv),
    // 注入变量到 html 文件
    createHtmlPlugin({
      minify: true,
      inject: {
        data: { title: VITE_GLOB_APP_TITLE }
      }
    }),
    // 使用 svg 图标
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), "src/assets/icons")],
      symbolId: "icon-[dir]-[name]"
    }),
    // vitePWA
    VITE_PWA && createVitePwa(viteEnv),
    // 是否生成包预览,分析依赖包大小做优化处理
    VITE_REPORT && (visualizer({ filename: "stats.html", gzipSize: true, brotliSize: true }) as PluginOption),
    // 自动 IDE 并将光标定位到 DOM 对应的源代码位置。see: https://inspector.fe-dev.cn/guide/start.html
    VITE_CODEINSPECTOR &&
      codeInspectorPlugin({
        bundler: "vite"
      })
  ];
};

/**
 * @description 根据 compress 配置,生成不同的压缩规则
 * @param viteEnv
 */
const createCompression = (viteEnv: ViteEnv): PluginOption | PluginOption[] => {
  const { VITE_BUILD_COMPRESS = "none", VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
  const compressList = VITE_BUILD_COMPRESS.split(",");
  const plugins: PluginOption[] = [];
  if (compressList.includes("gzip")) {
    plugins.push(
      viteCompression({
        ext: ".gz",
        algorithm: "gzip",
        deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
      })
    );
  }
  if (compressList.includes("brotli")) {
    plugins.push(
      viteCompression({
        ext: ".br",
        algorithm: "brotliCompress",
        deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
      })
    );
  }
  return plugins;
};

/**
 * @description VitePwa
 * @param viteEnv
 */
const createVitePwa = (viteEnv: ViteEnv): PluginOption | PluginOption[] => {
  const { VITE_GLOB_APP_TITLE } = viteEnv;
  return VitePWA({
    registerType: "autoUpdate",
    manifest: {
      name: VITE_GLOB_APP_TITLE,
      short_name: VITE_GLOB_APP_TITLE,
      theme_color: "#ffffff",
      icons: [
        {
          src: "/logo.png",
          sizes: "192x192",
          type: "image/png"
        },
        {
          src: "/logo.png",
          sizes: "512x512",
          type: "image/png"
        },
        {
          src: "/logo.png",
          sizes: "512x512",
          type: "image/png",
          purpose: "any maskable"
        }
      ]
    }
  });
};


================================================
FILE: build/proxy.ts
================================================
import type { ProxyOptions } from "vite";

type ProxyItem = [string, string];

type ProxyList = ProxyItem[];

type ProxyTargetList = Record<string, ProxyOptions>;

/**
 * 创建代理,用于解析 .env.development 代理配置
 * @param list
 */
export function createProxy(list: ProxyList = []) {
  const ret: ProxyTargetList = {};
  for (const [prefix, target] of list) {
    const httpsRE = /^https:\/\//;
    const isHttps = httpsRE.test(target);

    // https://github.com/http-party/node-http-proxy#options
    ret[prefix] = {
      target: target,
      changeOrigin: true,
      ws: true,
      rewrite: path => path.replace(new RegExp(`^${prefix}`), ""),
      // https is require secure=false
      ...(isHttps ? { secure: false } : {})
    };
  }
  return ret;
}


================================================
FILE: commitlint.config.cjs
================================================
// @see: https://cz-git.qbenben.com/zh/guide
const fs = require("fs");
const path = require("path");

const scopes = fs
  .readdirSync(path.resolve(__dirname, "src"), { withFileTypes: true })
  .filter(dirent => dirent.isDirectory())
  .map(dirent => dirent.name.replace(/s$/, ""));

/** @type {import('cz-git').UserConfig} */
module.exports = {
  ignores: [commit => commit.includes("init")],
  extends: ["@commitlint/config-conventional"],
  rules: {
    // @see: https://commitlint.js.org/#/reference-rules
    "body-leading-blank": [2, "always"],
    "footer-leading-blank": [1, "always"],
    "header-max-length": [2, "always", 108],
    "subject-empty": [2, "never"],
    "type-empty": [2, "never"],
    "subject-case": [0],
    "type-enum": [
      2,
      "always",
      [
        "feat",
        "fix",
        "docs",
        "style",
        "refactor",
        "perf",
        "test",
        "build",
        "ci",
        "chore",
        "revert",
        "wip",
        "workflow",
        "types",
        "release"
      ]
    ]
  },
  prompt: {
    messages: {
      type: "Select the type of change that you're committing:",
      scope: "Denote the SCOPE of this change (optional):",
      customScope: "Denote the SCOPE of this change:",
      subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
      body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
      breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
      footerPrefixsSelect: "Select the ISSUES type of changeList by this change (optional):",
      customFooterPrefixs: "Input ISSUES prefix:",
      footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
      confirmCommit: "Are you sure you want to proceed with the commit above?"
      // 中文版
      // type: "选择你要提交的类型 :",
      // scope: "选择一个提交范围(可选):",
      // customScope: "请输入自定义的提交范围 :",
      // subject: "填写简短精炼的变更描述 :\n",
      // body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
      // breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
      // footerPrefixsSelect: "选择关联issue前缀(可选):",
      // customFooterPrefixs: "输入自定义issue前缀 :",
      // footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
      // confirmCommit: "是否提交或修改commit ?"
    },
    types: [
      {
        value: "feat",
        name: "feat:     🚀  A new feature",
        emoji: "🚀"
      },
      {
        value: "fix",
        name: "fix:      🧩  A bug fix",
        emoji: "🧩"
      },
      {
        value: "docs",
        name: "docs:     📚  Documentation only changes",
        emoji: "📚"
      },
      {
        value: "style",
        name: "style:    🎨  Changes that do not affect the meaning of the code",
        emoji: "🎨"
      },
      {
        value: "refactor",
        name: "refactor: ♻️   A code change that neither fixes a bug nor adds a feature",
        emoji: "♻️"
      },
      {
        value: "perf",
        name: "perf:     ⚡️  A code change that improves performance",
        emoji: "⚡️"
      },
      {
        value: "test",
        name: "test:     ✅  Adding missing tests or correcting existing tests",
        emoji: "✅"
      },
      {
        value: "build",
        name: "build:    📦️   Changes that affect the build system or external dependencies",
        emoji: "📦️"
      },
      {
        value: "ci",
        name: "ci:       🎡  Changes to our CI configuration files and scripts",
        emoji: "🎡"
      },
      {
        value: "chore",
        name: "chore:    🔨  Other changes that don't modify src or test files",
        emoji: "🔨"
      },
      {
        value: "revert",
        name: "revert:   ⏪️  Reverts a previous commit",
        emoji: "⏪️"
      },
      {
        value: "wip",
        name: "wip:      🕔  work in process",
        emoji: "🕔"
      },
      {
        value: "workflow",
        name: "workflow: 📋  workflow improvements",
        emoji: "📋"
      },
      {
        value: "type",
        name: "type:     🔰  type definition file changes",
        emoji: "🔰"
      }
      // 中文版
      // { value: "feat", name: "特性:   🚀  新增功能", emoji: "🚀" },
      // { value: "fix", name: "修复:   🧩  修复缺陷", emoji: "🧩" },
      // { value: "docs", name: "文档:   📚  文档变更", emoji: "📚" },
      // { value: "style", name: "格式:   🎨  代码格式(不影响功能,例如空格、分号等格式修正)", emoji: "🎨" },
      // { value: "refactor", name: "重构:   ♻️  代码重构(不包括 bug 修复、功能新增)", emoji: "♻️" },
      // { value: "perf", name: "性能:    ⚡️  性能优化", emoji: "⚡️" },
      // { value: "test", name: "测试:   ✅  添加疏漏测试或已有测试改动", emoji: "✅" },
      // { value: "build", name: "构建:   📦️  构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)", emoji: "📦️" },
      // { value: "ci", name: "集成:   🎡  修改 CI 配置、脚本", emoji: "🎡" },
      // { value: "revert", name: "回退:   ⏪️  回滚 commit", emoji: "⏪️" },
      // { value: "chore", name: "其他:   🔨  对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: "🔨" },
      // { value: "wip", name: "开发:   🕔  正在开发中", emoji: "🕔" },
      // { value: "workflow", name: "工作流:   📋  工作流程改进", emoji: "📋" },
      // { value: "types", name: "类型:   🔰  类型定义文件修改", emoji: "🔰" }
    ],
    useEmoji: true,
    scopes: [...scopes],
    customScopesAlign: "bottom",
    emptyScopesAlias: "empty",
    customScopesAlias: "custom",
    allowBreakingChanges: ["feat", "fix"]
  }
};


================================================
FILE: index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vue.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%- title %></title>
  </head>
  <body>
    <div id="app">
      <style>
        html,
        body,
        #app {
          width: 100%;
          height: 100%;
          padding: 0;
          margin: 0;
        }
        .loading-box {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          width: 100%;
          height: 100%;
        }
        .loading-box .loading-wrap {
          display: flex;
          align-items: center;
          justify-content: center;
          padding: 98px;
        }
        .dot {
          position: relative;
          box-sizing: border-box;
          display: inline-block;
          width: 32px;
          height: 32px;
          font-size: 32px;
          transform: rotate(45deg);
          animation: ant-rotate 1.2s infinite linear;
        }
        .dot i {
          position: absolute;
          display: block;
          width: 14px;
          height: 14px;
          background-color: #409eff;
          border-radius: 100%;
          opacity: 0.3;
          transform: scale(0.75);
          transform-origin: 50% 50%;
          animation: ant-spin-move 1s infinite linear alternate;
        }
        .dot i:nth-child(1) {
          top: 0;
          left: 0;
        }
        .dot i:nth-child(2) {
          top: 0;
          right: 0;
          animation-delay: 0.4s;
        }
        .dot i:nth-child(3) {
          right: 0;
          bottom: 0;
          animation-delay: 0.8s;
        }
        .dot i:nth-child(4) {
          bottom: 0;
          left: 0;
          animation-delay: 1.2s;
        }

        @keyframes ant-rotate {
          to {
            transform: rotate(405deg);
          }
        }

        @keyframes ant-spin-move {
          to {
            opacity: 1;
          }
        }
      </style>
      <div class="loading-box">
        <div class="loading-wrap">
          <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
        </div>
      </div>
    </div>
    <script>
      const globalState = JSON.parse(window.localStorage.getItem("geeker-global"));
      if (globalState) {
        const dot = document.querySelectorAll(".dot i");
        const html = document.querySelector("html");
        dot.forEach(item => (item.style.background = globalState.primary));
        if (globalState.isDark) html.style.background = "#141414";
      }
    </script>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: lint-staged.config.cjs
================================================
module.exports = {
  "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
  "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"],
  "package.json": ["prettier --write"],
  "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
  "*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"],
  "*.md": ["prettier --write"]
};


================================================
FILE: package.json
================================================
{
  "name": "geeker-admin",
  "private": true,
  "version": "1.2.0",
  "type": "module",
  "description": "geeker-admin open source management system",
  "author": {
    "name": "Geeker",
    "email": "848130454@qq.com",
    "url": "https://github.com/HalseySpicy"
  },
  "license": "MIT",
  "homepage": "https://github.com/HalseySpicy/Geeker-Admin",
  "repository": {
    "type": "git",
    "url": "git@github.com:HalseySpicy/Geeker-Admin.git"
  },
  "bugs": {
    "url": "https://github.com/HalseySpicy/Geeker-Admin/issues"
  },
  "scripts": {
    "dev": "vite",
    "serve": "vite",
    "build:dev": "vue-tsc && vite build --mode development",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "type:check": "vue-tsc --noEmit --skipLibCheck",
    "preview": "pnpm build:dev && vite preview",
    "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
    "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
    "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
    "lint:lint-staged": "lint-staged",
    "prepare": "husky install",
    "release": "standard-version",
    "commit": "git add -A && czg && git push"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "@vueuse/core": "^10.11.0",
    "@wangeditor/editor": "^5.1.23",
    "@wangeditor/editor-for-vue": "^5.1.12",
    "axios": "^1.7.2",
    "dayjs": "^1.11.11",
    "driver.js": "^1.3.1",
    "echarts": "^5.5.1",
    "echarts-liquidfill": "^3.1.0",
    "element-plus": "^2.7.6",
    "md5": "^2.3.0",
    "mitt": "^3.0.1",
    "nprogress": "^0.2.0",
    "pinia": "^2.1.7",
    "pinia-plugin-persistedstate": "^3.2.1",
    "qs": "^6.12.1",
    "screenfull": "^6.0.2",
    "sortablejs": "^1.15.2",
    "vue": "^3.4.31",
    "vue-i18n": "^9.13.1",
    "vue-router": "^4.4.0",
    "vuedraggable": "^4.1.0"
  },
  "devDependencies": {
    "@commitlint/cli": "^18.4.3",
    "@commitlint/config-conventional": "^18.4.3",
    "@types/md5": "^2.3.5",
    "@types/nprogress": "^0.2.3",
    "@types/qs": "^6.9.15",
    "@types/sortablejs": "^1.15.8",
    "@typescript-eslint/eslint-plugin": "^7.14.1",
    "@typescript-eslint/parser": "^7.14.1",
    "@vitejs/plugin-vue": "^5.0.4",
    "@vitejs/plugin-vue-jsx": "^3.1.0",
    "autoprefixer": "^10.4.19",
    "code-inspector-plugin": "^0.16.1",
    "cz-git": "1.9.2",
    "czg": "^1.9.2",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-vue": "^9.26.0",
    "husky": "^9.0.11",
    "lint-staged": "^15.2.5",
    "postcss": "^8.4.38",
    "postcss-html": "^1.7.0",
    "prettier": "^3.3.2",
    "rollup-plugin-visualizer": "^5.12.0",
    "sass": "^1.77.6",
    "standard-version": "^9.5.0",
    "stylelint": "^16.6.1",
    "stylelint-config-html": "^1.1.0",
    "stylelint-config-recess-order": "^5.0.1",
    "stylelint-config-recommended-scss": "^14.0.0",
    "stylelint-config-recommended-vue": "^1.5.0",
    "stylelint-config-standard": "^36.0.0",
    "stylelint-config-standard-scss": "^13.1.0",
    "typescript": "^5.5.2",
    "unplugin-vue-setup-extend-plus": "^1.0.1",
    "vite": "^5.3.2",
    "vite-plugin-compression": "^0.5.1",
    "vite-plugin-eslint": "^1.8.1",
    "vite-plugin-html": "^3.2.2",
    "vite-plugin-pwa": "^0.20.0",
    "vite-plugin-svg-icons": "^2.0.1",
    "vite-plugin-vue-devtools": "^7.3.5",
    "vue-tsc": "^2.0.22"
  },
  "engines": {
    "node": ">=16.18.0"
  },
  "browserslist": {
    "production": [
      "> 1%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }
}


================================================
FILE: postcss.config.cjs
================================================
module.exports = {
  plugins: {
    autoprefixer: {}
  }
};


================================================
FILE: src/App.vue
================================================
<template>
  <el-config-provider :locale="locale" :size="assemblySize" :button="buttonConfig">
    <router-view></router-view>
  </el-config-provider>
</template>

<script setup lang="ts">
import { onMounted, reactive, computed } from "vue";
import { useI18n } from "vue-i18n";
import { getBrowserLang } from "@/utils";
import { useTheme } from "@/hooks/useTheme";
import { ElConfigProvider } from "element-plus";
import { LanguageType } from "./stores/interface";
import { useGlobalStore } from "@/stores/modules/global";
import en from "element-plus/es/locale/lang/en";
import zhCn from "element-plus/es/locale/lang/zh-cn";

const globalStore = useGlobalStore();

// init theme
const { initTheme } = useTheme();
initTheme();

// init language
const i18n = useI18n();
onMounted(() => {
  const language = globalStore.language ?? getBrowserLang();
  i18n.locale.value = language;
  globalStore.setGlobalState("language", language as LanguageType);
});

// element language
const locale = computed(() => {
  if (globalStore.language == "zh") return zhCn;
  if (globalStore.language == "en") return en;
  return getBrowserLang() == "zh" ? zhCn : en;
});

// element assemblySize
const assemblySize = computed(() => globalStore.assemblySize);

// element button config
const buttonConfig = reactive({ autoInsertSpace: false });
</script>


================================================
FILE: src/api/config/servicePort.ts
================================================
// 后端微服务模块前缀
export const PORT1 = "/geeker";
export const PORT2 = "/hooks";


================================================
FILE: src/api/helper/axiosCancel.ts
================================================
import { CustomAxiosRequestConfig } from "../index";
import qs from "qs";

// 声明一个 Map 用于存储每个请求的标识和取消函数
let pendingMap = new Map<string, AbortController>();

// 序列化参数,确保对象属性顺序一致
const sortedStringify = (obj: any) => {
  return qs.stringify(obj, { arrayFormat: "repeat", sort: (a, b) => a.localeCompare(b) });
};

// 获取请求的唯一标识
export const getPendingUrl = (config: CustomAxiosRequestConfig) => {
  return [config.method, config.url, sortedStringify(config.data), sortedStringify(config.params)].join("&");
};

export class AxiosCanceler {
  /**
   * @description: 添加请求
   * @param {Object} config
   * @return void
   */
  addPending(config: CustomAxiosRequestConfig) {
    // 在请求开始前,对之前的请求做检查取消操作
    this.removePending(config);
    const url = getPendingUrl(config);
    const controller = new AbortController();
    config.signal = controller.signal;
    pendingMap.set(url, controller);
  }

  /**
   * @description: 移除请求
   * @param {Object} config
   */
  removePending(config: CustomAxiosRequestConfig) {
    const url = getPendingUrl(config);
    // 如果在 pending 中存在当前请求标识,需要取消当前请求并删除条目
    const controller = pendingMap.get(url);
    if (controller) {
      controller.abort();
      pendingMap.delete(url);
    }
  }

  /**
   * @description: 清空所有pending
   */
  removeAllPending() {
    pendingMap.forEach(controller => {
      controller && controller.abort();
    });
    pendingMap.clear();
  }
}


================================================
FILE: src/api/helper/checkStatus.ts
================================================
import { ElMessage } from "element-plus";

/**
 * @description: 校验网络请求状态码
 * @param {Number} status
 * @return void
 */
export const checkStatus = (status: number) => {
  switch (status) {
    case 400:
      ElMessage.error("请求失败!请您稍后重试");
      break;
    case 401:
      ElMessage.error("登录失效!请您重新登录");
      break;
    case 403:
      ElMessage.error("当前账号无权限访问!");
      break;
    case 404:
      ElMessage.error("你所访问的资源不存在!");
      break;
    case 405:
      ElMessage.error("请求方式错误!请您稍后重试");
      break;
    case 408:
      ElMessage.error("请求超时!请您稍后重试");
      break;
    case 500:
      ElMessage.error("服务异常!");
      break;
    case 502:
      ElMessage.error("网关错误!");
      break;
    case 503:
      ElMessage.error("服务不可用!");
      break;
    case 504:
      ElMessage.error("网关超时!");
      break;
    default:
      ElMessage.error("请求失败!");
  }
};


================================================
FILE: src/api/index.ts
================================================
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
import { LOGIN_URL } from "@/config";
import { ElMessage } from "element-plus";
import { ResultData } from "@/api/interface";
import { ResultEnum } from "@/enums/httpEnum";
import { checkStatus } from "./helper/checkStatus";
import { AxiosCanceler } from "./helper/axiosCancel";
import { useUserStore } from "@/stores/modules/user";
import router from "@/routers";

export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
  loading?: boolean;
  cancel?: boolean;
}

const config = {
  // 默认地址请求地址,可在 .env.** 文件中修改
  baseURL: import.meta.env.VITE_API_URL as string,
  // 设置超时时间
  timeout: ResultEnum.TIMEOUT as number,
  // 跨域时候允许携带凭证
  withCredentials: true
};

const axiosCanceler = new AxiosCanceler();

class RequestHttp {
  service: AxiosInstance;
  public constructor(config: AxiosRequestConfig) {
    // instantiation
    this.service = axios.create(config);

    /**
     * @description 请求拦截器
     * 客户端发送请求 -> [请求拦截器] -> 服务器
     * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
     */
    this.service.interceptors.request.use(
      (config: CustomAxiosRequestConfig) => {
        const userStore = useUserStore();
        // 重复请求不需要取消,在 api 服务中通过指定的第三个参数: { cancel: false } 来控制
        config.cancel ??= true;
        config.cancel && axiosCanceler.addPending(config);
        // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制
        config.loading ??= true;
        config.loading && showFullScreenLoading();
        if (config.headers && typeof config.headers.set === "function") {
          config.headers.set("x-access-token", userStore.token);
        }
        return config;
      },
      (error: AxiosError) => {
        return Promise.reject(error);
      }
    );

    /**
     * @description 响应拦截器
     *  服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
     */
    this.service.interceptors.response.use(
      (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
        const { data, config } = response;

        const userStore = useUserStore();
        axiosCanceler.removePending(config);
        config.loading && tryHideFullScreenLoading();
        // 登录失效
        if (data.code == ResultEnum.OVERDUE) {
          userStore.setToken("");
          router.replace(LOGIN_URL);
          ElMessage.error(data.msg);
          return Promise.reject(data);
        }
        // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
        if (data.code && data.code !== ResultEnum.SUCCESS) {
          ElMessage.error(data.msg);
          return Promise.reject(data);
        }
        // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
        return data;
      },
      async (error: AxiosError) => {
        const { response } = error;
        tryHideFullScreenLoading();
        // 请求超时 && 网络错误单独判断,没有 response
        if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
        if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
        // 根据服务器响应的错误状态码,做不同的处理
        if (response) checkStatus(response.status);
        // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
        if (!window.navigator.onLine) router.replace("/500");
        return Promise.reject(error);
      }
    );
  }

  /**
   * @description 常用请求方法封装
   */
  get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
    return this.service.get(url, { params, ..._object });
  }
  post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
    return this.service.post(url, params, _object);
  }
  put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
    return this.service.put(url, params, _object);
  }
  delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
    return this.service.delete(url, { params, ..._object });
  }
  download(url: string, params?: object, _object = {}): Promise<BlobPart> {
    return this.service.post(url, params, { ..._object, responseType: "blob" });
  }
}

export default new RequestHttp(config);


================================================
FILE: src/api/interface/index.ts
================================================
// 请求响应参数(不包含data)
export interface Result {
  code: string;
  msg: string;
}

// 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
  data: T;
}

// 分页响应参数
export interface ResPage<T> {
  list: T[];
  pageNum: number;
  pageSize: number;
  total: number;
}

// 分页请求参数
export interface ReqPage {
  pageNum: number;
  pageSize: number;
}

// 文件上传模块
export namespace Upload {
  export interface ResFileUrl {
    fileUrl: string;
  }
}

// 登录模块
export namespace Login {
  export interface ReqLoginForm {
    username: string;
    password: string;
  }
  export interface ResLogin {
    access_token: string;
  }
  export interface ResAuthButtons {
    [key: string]: string[];
  }
}

// 用户管理模块
export namespace User {
  export interface ReqUserParams extends ReqPage {
    username: string;
    gender: number;
    idCard: string;
    email: string;
    address: string;
    createTime: string[];
    status: number;
  }
  export interface ResUserList {
    id: string;
    username: string;
    gender: number;
    user: { detail: { age: number } };
    idCard: string;
    email: string;
    address: string;
    createTime: string;
    status: number;
    avatar: string;
    photo: any[];
    children?: ResUserList[];
  }
  export interface ResStatus {
    userLabel: string;
    userValue: number;
  }
  export interface ResGender {
    genderLabel: string;
    genderValue: number;
  }
  export interface ResDepartment {
    id: string;
    name: string;
    children?: ResDepartment[];
  }
  export interface ResRole {
    id: string;
    name: string;
    children?: ResDepartment[];
  }
}


================================================
FILE: src/api/modules/login.ts
================================================
import { Login } from "@/api/interface/index";
import { PORT1 } from "@/api/config/servicePort";
import authMenuList from "@/assets/json/authMenuList.json";
import authButtonList from "@/assets/json/authButtonList.json";
import http from "@/api";

/**
 * @name 登录模块
 */
// 用户登录
export const loginApi = (params: Login.ReqLoginForm) => {
  return http.post<Login.ResLogin>(PORT1 + `/login`, params, { loading: false }); // 正常 post json 请求  ==>  application/json
  // return http.post<Login.ResLogin>(PORT1 + `/login`, params, { loading: false }); // 控制当前请求不显示 loading
  // return http.post<Login.ResLogin>(PORT1 + `/login`, {}, { params }); // post 请求携带 query 参数  ==>  ?username=admin&password=123456
  // return http.post<Login.ResLogin>(PORT1 + `/login`, qs.stringify(params)); // post 请求携带表单参数  ==>  application/x-www-form-urlencoded
  // return http.get<Login.ResLogin>(PORT1 + `/login?${qs.stringify(params, { arrayFormat: "repeat" })}`); // get 请求可以携带数组等复杂参数
};

// 获取菜单列表
export const getAuthMenuListApi = () => {
  return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`, {}, { loading: false });
  // 如果想让菜单变为本地数据,注释上一行代码,并引入本地 authMenuList.json 数据
  return authMenuList;
};

// 获取按钮权限
export const getAuthButtonListApi = () => {
  return http.get<Login.ResAuthButtons>(PORT1 + `/auth/buttons`, {}, { loading: false });
  // 如果想让按钮权限变为本地数据,注释上一行代码,并引入本地 authButtonList.json 数据
  return authButtonList;
};

// 用户退出登录
export const logoutApi = () => {
  return http.post(PORT1 + `/logout`);
};


================================================
FILE: src/api/modules/upload.ts
================================================
import { Upload } from "@/api/interface/index";
import { PORT1 } from "@/api/config/servicePort";
import http from "@/api";

/**
 * @name 文件上传模块
 */
// 图片上传
export const uploadImg = (params: FormData) => {
  return http.post<Upload.ResFileUrl>(PORT1 + `/file/upload/img`, params, { cancel: false });
};

// 视频上传
export const uploadVideo = (params: FormData) => {
  return http.post<Upload.ResFileUrl>(PORT1 + `/file/upload/video`, params, { cancel: false });
};


================================================
FILE: src/api/modules/user.ts
================================================
import { ResPage, User } from "@/api/interface/index";
import { PORT1 } from "@/api/config/servicePort";
import http from "@/api";

/**
 * @name 用户管理模块
 */
// 获取用户列表
export const getUserList = (params: User.ReqUserParams) => {
  return http.post<ResPage<User.ResUserList>>(PORT1 + `/user/list`, params);
};

// 获取树形用户列表
export const getUserTreeList = (params: User.ReqUserParams) => {
  return http.post<ResPage<User.ResUserList>>(PORT1 + `/user/tree/list`, params);
};

// 新增用户
export const addUser = (params: { id: string }) => {
  return http.post(PORT1 + `/user/add`, params);
};

// 批量添加用户
export const BatchAddUser = (params: FormData) => {
  return http.post(PORT1 + `/user/import`, params);
};

// 编辑用户
export const editUser = (params: { id: string }) => {
  return http.post(PORT1 + `/user/edit`, params);
};

// 删除用户
export const deleteUser = (params: { id: string[] }) => {
  return http.post(PORT1 + `/user/delete`, params);
};

// 切换用户状态
export const changeUserStatus = (params: { id: string; status: number }) => {
  return http.post(PORT1 + `/user/change`, params);
};

// 重置用户密码
export const resetUserPassWord = (params: { id: string }) => {
  return http.post(PORT1 + `/user/rest_password`, params);
};

// 导出用户数据
export const exportUserInfo = (params: User.ReqUserParams) => {
  return http.download(PORT1 + `/user/export`, params);
};

// 获取用户状态字典
export const getUserStatus = () => {
  return http.get<User.ResStatus[]>(PORT1 + `/user/status`);
};

// 获取用户性别字典
export const getUserGender = () => {
  return http.get<User.ResGender[]>(PORT1 + `/user/gender`);
};

// 获取用户部门列表
export const getUserDepartment = () => {
  return http.get<User.ResDepartment[]>(PORT1 + `/user/department`, {}, { cancel: false });
};

// 获取用户角色字典
export const getUserRole = () => {
  return http.get<User.ResRole[]>(PORT1 + `/user/role`);
};


================================================
FILE: src/assets/fonts/font.scss
================================================
@font-face {
  font-family: YouSheBiaoTiHei;
  src: url("./YouSheBiaoTiHei.ttf");
}

@font-face {
  font-family: MetroDF;
  src: url("./MetroDF.ttf");
}

@font-face {
  font-family: DIN;
  src: url("./DIN.otf");
}


================================================
FILE: src/assets/iconfont/iconfont.scss
================================================
@font-face {
  font-family: iconfont; /* Project id 2667653 */
  src: url("iconfont.ttf?t=1719667796161") format("truetype");
}
.iconfont {
  font-family: iconfont !important;
  font-size: 20px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  cursor: pointer;
}
.icon-yiwen::before {
  font-size: 15px;
  content: "\e693";
}
.icon-xiala::before {
  content: "\e62b";
}
.icon-tuichu::before {
  content: "\e645";
}
.icon-xiaoxi::before {
  font-size: 21.2px;
  content: "\e61f";
}
.icon-zhuti::before {
  font-size: 22.4px;
  content: "\e638";
}
.icon-sousuo::before {
  content: "\e611";
}
.icon-contentright::before {
  content: "\e8c9";
}
.icon-contentleft::before {
  content: "\e8ca";
}
.icon-fangda::before {
  content: "\e826";
}
.icon-suoxiao::before {
  content: "\e641";
}
.icon-zhongyingwen::before {
  content: "\e8cb";
}
.icon-huiche::before {
  content: "\e637";
}


================================================
FILE: src/assets/json/authButtonList.json
================================================
{
  "code": 200,
  "data": {
    "useProTable": ["add", "batchAdd", "export", "batchDelete", "status"],
    "authButton": ["add", "edit", "delete", "import", "export"]
  },
  "msg": "成功"
}


================================================
FILE: src/assets/json/authMenuList.json
================================================
{
  "code": 200,
  "data": [
    {
      "path": "/home/index",
      "name": "home",
      "component": "/home/index",
      "meta": {
        "icon": "HomeFilled",
        "title": "首页",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": true,
        "isKeepAlive": true
      }
    },
    {
      "path": "/dataScreen",
      "name": "dataScreen",
      "component": "/dataScreen/index",
      "meta": {
        "icon": "Histogram",
        "title": "数据大屏",
        "isLink": "",
        "isHide": false,
        "isFull": true,
        "isAffix": false,
        "isKeepAlive": true
      }
    },
    {
      "path": "/proTable",
      "name": "proTable",
      "redirect": "/proTable/useProTable",
      "meta": {
        "icon": "MessageBox",
        "title": "超级表格",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/proTable/useProTable",
          "name": "useProTable",
          "component": "/proTable/useProTable/index",
          "meta": {
            "icon": "Menu",
            "title": "使用 ProTable",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          },
          "children": [
            {
              "path": "/proTable/useProTable/detail/:id",
              "name": "useProTableDetail",
              "component": "/proTable/useProTable/detail",
              "meta": {
                "icon": "Menu",
                "title": "ProTable 详情",
                "activeMenu": "/proTable/useProTable",
                "isLink": "",
                "isHide": true,
                "isFull": false,
                "isAffix": false,
                "isKeepAlive": true
              }
            }
          ]
        },
        {
          "path": "/proTable/useTreeFilter",
          "name": "useTreeFilter",
          "component": "/proTable/useTreeFilter/index",
          "meta": {
            "icon": "Menu",
            "title": "使用 TreeFilter",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/proTable/useTreeFilter/detail/:id",
          "name": "useTreeFilterDetail",
          "component": "/proTable/useTreeFilter/detail",
          "meta": {
            "icon": "Menu",
            "title": "TreeFilter 详情",
            "activeMenu": "/proTable/useTreeFilter",
            "isLink": "",
            "isHide": true,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/proTable/useSelectFilter",
          "name": "useSelectFilter",
          "component": "/proTable/useSelectFilter/index",
          "meta": {
            "icon": "Menu",
            "title": "使用 SelectFilter",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/proTable/treeProTable",
          "name": "treeProTable",
          "component": "/proTable/treeProTable/index",
          "meta": {
            "icon": "Menu",
            "title": "树形 ProTable",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/proTable/complexProTable",
          "name": "complexProTable",
          "component": "/proTable/complexProTable/index",
          "meta": {
            "icon": "Menu",
            "title": "复杂 ProTable",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/proTable/document",
          "name": "proTableDocument",
          "component": "/proTable/document/index",
          "meta": {
            "icon": "Menu",
            "title": "ProTable 文档",
            "isLink": "https://juejin.cn/post/7166068828202336263/#heading-14",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/auth",
      "name": "auth",
      "redirect": "/auth/menu",
      "meta": {
        "icon": "Lock",
        "title": "权限管理",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/auth/menu",
          "name": "authMenu",
          "component": "/auth/menu/index",
          "meta": {
            "icon": "Menu",
            "title": "菜单权限",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/auth/button",
          "name": "authButton",
          "component": "/auth/button/index",
          "meta": {
            "icon": "Menu",
            "title": "按钮权限",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/assembly",
      "name": "assembly",
      "redirect": "/assembly/guide",
      "meta": {
        "icon": "Briefcase",
        "title": "常用组件",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/assembly/guide",
          "name": "guide",
          "component": "/assembly/guide/index",
          "meta": {
            "icon": "Menu",
            "title": "引导页",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/tabs",
          "name": "tabs",
          "component": "/assembly/tabs/index",
          "meta": {
            "icon": "Menu",
            "title": "标签页操作",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          },
          "children": [
            {
              "path": "/assembly/tabs/detail/:id",
              "name": "tabsDetail",
              "component": "/assembly/tabs/detail",
              "meta": {
                "icon": "Menu",
                "title": "Tab 详情",
                "activeMenu": "/assembly/tabs",
                "isLink": "",
                "isHide": true,
                "isFull": false,
                "isAffix": false,
                "isKeepAlive": true
              }
            }
          ]
        },
        {
          "path": "/assembly/selectIcon",
          "name": "selectIcon",
          "component": "/assembly/selectIcon/index",
          "meta": {
            "icon": "Menu",
            "title": "图标选择器",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/selectFilter",
          "name": "selectFilter",
          "component": "/assembly/selectFilter/index",
          "meta": {
            "icon": "Menu",
            "title": "分类筛选器",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/treeFilter",
          "name": "treeFilter",
          "component": "/assembly/treeFilter/index",
          "meta": {
            "icon": "Menu",
            "title": "树形筛选器",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/svgIcon",
          "name": "svgIcon",
          "component": "/assembly/svgIcon/index",
          "meta": {
            "icon": "Menu",
            "title": "SVG 图标",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/uploadFile",
          "name": "uploadFile",
          "component": "/assembly/uploadFile/index",
          "meta": {
            "icon": "Menu",
            "title": "文件上传",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/batchImport",
          "name": "batchImport",
          "component": "/assembly/batchImport/index",
          "meta": {
            "icon": "Menu",
            "title": "批量添加数据",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/wangEditor",
          "name": "wangEditor",
          "component": "/assembly/wangEditor/index",
          "meta": {
            "icon": "Menu",
            "title": "富文本编辑器",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/assembly/draggable",
          "name": "draggable",
          "component": "/assembly/draggable/index",
          "meta": {
            "icon": "Menu",
            "title": "拖拽组件",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/dashboard",
      "name": "dashboard",
      "redirect": "/dashboard/dataVisualize",
      "meta": {
        "icon": "Odometer",
        "title": "Dashboard",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/dashboard/dataVisualize",
          "name": "dataVisualize",
          "component": "/dashboard/dataVisualize/index",
          "meta": {
            "icon": "Menu",
            "title": "数据可视化",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/form",
      "name": "form",
      "redirect": "/form/proForm",
      "meta": {
        "icon": "Tickets",
        "title": "表单 Form",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/form/proForm",
          "name": "proForm",
          "component": "/form/proForm/index",
          "meta": {
            "icon": "Menu",
            "title": "超级 Form",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/form/basicForm",
          "name": "basicForm",
          "component": "/form/basicForm/index",
          "meta": {
            "icon": "Menu",
            "title": "基础 Form",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/form/validateForm",
          "name": "validateForm",
          "component": "/form/validateForm/index",
          "meta": {
            "icon": "Menu",
            "title": "校验 Form",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/form/dynamicForm",
          "name": "dynamicForm",
          "component": "/form/dynamicForm/index",
          "meta": {
            "icon": "Menu",
            "title": "动态 Form",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/echarts",
      "name": "echarts",
      "redirect": "/echarts/waterChart",
      "meta": {
        "icon": "TrendCharts",
        "title": "ECharts",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/echarts/waterChart",
          "name": "waterChart",
          "component": "/echarts/waterChart/index",
          "meta": {
            "icon": "Menu",
            "title": "水型图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/echarts/columnChart",
          "name": "columnChart",
          "component": "/echarts/columnChart/index",
          "meta": {
            "icon": "Menu",
            "title": "柱状图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/echarts/lineChart",
          "name": "lineChart",
          "component": "/echarts/lineChart/index",
          "meta": {
            "icon": "Menu",
            "title": "折线图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/echarts/pieChart",
          "name": "pieChart",
          "component": "/echarts/pieChart/index",
          "meta": {
            "icon": "Menu",
            "title": "饼图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/echarts/radarChart",
          "name": "radarChart",
          "component": "/echarts/radarChart/index",
          "meta": {
            "icon": "Menu",
            "title": "雷达图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/echarts/nestedChart",
          "name": "nestedChart",
          "component": "/echarts/nestedChart/index",
          "meta": {
            "icon": "Menu",
            "title": "嵌套环形图",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/directives",
      "name": "directives",
      "redirect": "/directives/copyDirect",
      "meta": {
        "icon": "Stamp",
        "title": "自定义指令",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/directives/copyDirect",
          "name": "copyDirect",
          "component": "/directives/copyDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "复制指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/directives/watermarkDirect",
          "name": "watermarkDirect",
          "component": "/directives/watermarkDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "水印指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/directives/dragDirect",
          "name": "dragDirect",
          "component": "/directives/dragDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "拖拽指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/directives/debounceDirect",
          "name": "debounceDirect",
          "component": "/directives/debounceDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "防抖指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/directives/throttleDirect",
          "name": "throttleDirect",
          "component": "/directives/throttleDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "节流指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/directives/longpressDirect",
          "name": "longpressDirect",
          "component": "/directives/longpressDirect/index",
          "meta": {
            "icon": "Menu",
            "title": "长按指令",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/menu",
      "name": "menu",
      "redirect": "/menu/menu1",
      "meta": {
        "icon": "List",
        "title": "菜单嵌套",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/menu/menu1",
          "name": "menu1",
          "component": "/menu/menu1/index",
          "meta": {
            "icon": "Menu",
            "title": "菜单1",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/menu/menu2",
          "name": "menu2",
          "redirect": "/menu/menu2/menu21",
          "meta": {
            "icon": "Menu",
            "title": "菜单2",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          },
          "children": [
            {
              "path": "/menu/menu2/menu21",
              "name": "menu21",
              "component": "/menu/menu2/menu21/index",
              "meta": {
                "icon": "Menu",
                "title": "菜单2-1",
                "isLink": "",
                "isHide": false,
                "isFull": false,
                "isAffix": false,
                "isKeepAlive": true
              }
            },
            {
              "path": "/menu/menu2/menu22",
              "name": "menu22",
              "redirect": "/menu/menu2/menu22/menu221",
              "meta": {
                "icon": "Menu",
                "title": "菜单2-2",
                "isLink": "",
                "isHide": false,
                "isFull": false,
                "isAffix": false,
                "isKeepAlive": true
              },
              "children": [
                {
                  "path": "/menu/menu2/menu22/menu221",
                  "name": "menu221",
                  "component": "/menu/menu2/menu22/menu221/index",
                  "meta": {
                    "icon": "Menu",
                    "title": "菜单2-2-1",
                    "isLink": "",
                    "isHide": false,
                    "isFull": false,
                    "isAffix": false,
                    "isKeepAlive": true
                  }
                },
                {
                  "path": "/menu/menu2/menu22/menu222",
                  "name": "menu222",
                  "component": "/menu/menu2/menu22/menu222/index",
                  "meta": {
                    "icon": "Menu",
                    "title": "菜单2-2-2",
                    "isLink": "",
                    "isHide": false,
                    "isFull": false,
                    "isAffix": false,
                    "isKeepAlive": true
                  }
                }
              ]
            },
            {
              "path": "/menu/menu2/menu23",
              "name": "menu23",
              "component": "/menu/menu2/menu23/index",
              "meta": {
                "icon": "Menu",
                "title": "菜单2-3",
                "isLink": "",
                "isHide": false,
                "isFull": false,
                "isAffix": false,
                "isKeepAlive": true
              }
            }
          ]
        },
        {
          "path": "/menu/menu3",
          "name": "menu3",
          "component": "/menu/menu3/index",
          "meta": {
            "icon": "Menu",
            "title": "菜单3",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/system",
      "name": "system",
      "redirect": "/system/accountManage",
      "meta": {
        "icon": "Tools",
        "title": "系统管理",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/system/accountManage",
          "name": "accountManage",
          "component": "/system/accountManage/index",
          "meta": {
            "icon": "Menu",
            "title": "账号管理",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/roleManage",
          "name": "roleManage",
          "component": "/system/roleManage/index",
          "meta": {
            "icon": "Menu",
            "title": "角色管理",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/menuMange",
          "name": "menuMange",
          "component": "/system/menuMange/index",
          "meta": {
            "icon": "Menu",
            "title": "菜单管理",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/departmentManage",
          "name": "departmentManage",
          "component": "/system/departmentManage/index",
          "meta": {
            "icon": "Menu",
            "title": "部门管理",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/dictManage",
          "name": "dictManage",
          "component": "/system/dictManage/index",
          "meta": {
            "icon": "Menu",
            "title": "字典管理",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/timingTask",
          "name": "timingTask",
          "component": "/system/timingTask/index",
          "meta": {
            "icon": "Menu",
            "title": "定时任务",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/system/systemLog",
          "name": "systemLog",
          "component": "/system/systemLog/index",
          "meta": {
            "icon": "Menu",
            "title": "系统日志",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/link",
      "name": "link",
      "redirect": "/link/bing",
      "meta": {
        "icon": "Paperclip",
        "title": "外部链接",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/link/bing",
          "name": "bing",
          "component": "/link/bing/index",
          "meta": {
            "icon": "Menu",
            "title": "Bing 内嵌",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/link/gitee",
          "name": "gitee",
          "component": "/link/gitee/index",
          "meta": {
            "icon": "Menu",
            "title": "Gitee 仓库",
            "isLink": "https://gitee.com/HalseySpicy/Geeker-Admin",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/link/github",
          "name": "github",
          "component": "/link/github/index",
          "meta": {
            "icon": "Menu",
            "title": "GitHub 仓库",
            "isLink": "https://github.com/HalseySpicy/Geeker-Admin",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/link/docs",
          "name": "docs",
          "component": "/link/docs/index",
          "meta": {
            "icon": "Menu",
            "title": "项目文档",
            "isLink": "https://docs.spicyboy.cn",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        },
        {
          "path": "/link/juejin",
          "name": "juejin",
          "component": "/link/juejin/index",
          "meta": {
            "icon": "Menu",
            "title": "掘金主页",
            "isLink": "https://juejin.cn/user/3263814531551816/posts",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    },
    {
      "path": "/about/index",
      "name": "about",
      "component": "/about/index",
      "meta": {
        "icon": "InfoFilled",
        "title": "关于项目",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      }
    }
  ],
  "msg": "成功"
}


================================================
FILE: src/components/ECharts/config/index.ts
================================================
import * as echarts from "echarts/core";
import { BarChart, LineChart, LinesChart, PieChart, ScatterChart, RadarChart, GaugeChart } from "echarts/charts";
import {
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  LegendComponent,
  PolarComponent,
  GeoComponent,
  ToolboxComponent,
  DataZoomComponent
} from "echarts/components";
import { LabelLayout, UniversalTransition } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers";
import type {
  BarSeriesOption,
  LineSeriesOption,
  LinesSeriesOption,
  PieSeriesOption,
  ScatterSeriesOption,
  RadarSeriesOption,
  GaugeSeriesOption
} from "echarts/charts";
import type {
  TitleComponentOption,
  TooltipComponentOption,
  GridComponentOption,
  DatasetComponentOption
} from "echarts/components";
import type { ComposeOption } from "echarts/core";
import "echarts-liquidfill";

export type ECOption = ComposeOption<
  | BarSeriesOption
  | LineSeriesOption
  | LinesSeriesOption
  | PieSeriesOption
  | RadarSeriesOption
  | GaugeSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | DatasetComponentOption
  | ScatterSeriesOption
>;

echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  LegendComponent,
  PolarComponent,
  GeoComponent,
  ToolboxComponent,
  DataZoomComponent,
  BarChart,
  LineChart,
  LinesChart,
  PieChart,
  ScatterChart,
  RadarChart,
  GaugeChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer
]);

export default echarts;


================================================
FILE: src/components/ECharts/index.vue
================================================
<template>
  <div id="echarts" ref="chartRef" :style="echartsStyle" />
</template>

<script setup lang="ts" name="ECharts">
import { ref, onMounted, onBeforeUnmount, watch, computed, markRaw, nextTick, onActivated } from "vue";
import { EChartsType, ECElementEvent } from "echarts/core";
import echarts, { ECOption } from "./config";
import { useDebounceFn } from "@vueuse/core";
import { useGlobalStore } from "@/stores/modules/global";
import { storeToRefs } from "pinia";

interface Props {
  option: ECOption;
  renderer?: "canvas" | "svg";
  resize?: boolean;
  theme?: Object | string;
  width?: number | string;
  height?: number | string;
  onClick?: (event: ECElementEvent) => any;
}

const props = withDefaults(defineProps<Props>(), {
  renderer: "canvas",
  resize: true
});

const echartsStyle = computed(() => {
  return props.width || props.height
    ? { height: props.height + "px", width: props.width + "px" }
    : { height: "100%", width: "100%" };
});

const chartRef = ref<HTMLDivElement | HTMLCanvasElement>();
const chartInstance = ref<EChartsType>();

const draw = () => {
  if (chartInstance.value) {
    chartInstance.value.setOption(props.option, { notMerge: true });
  }
};

watch(props, () => {
  draw();
});

const handleClick = (event: ECElementEvent) => props.onClick && props.onClick(event);

const init = () => {
  if (!chartRef.value) return;
  chartInstance.value = echarts.getInstanceByDom(chartRef.value);

  if (!chartInstance.value) {
    chartInstance.value = markRaw(
      echarts.init(chartRef.value, props.theme, {
        renderer: props.renderer
      })
    );
    chartInstance.value.on("click", handleClick);
    draw();
  }
};

const resize = () => {
  if (chartInstance.value && props.resize) {
    chartInstance.value.resize({ animation: { duration: 300 } });
  }
};

const debouncedResize = useDebounceFn(resize, 300, { maxWait: 800 });

const globalStore = useGlobalStore();
const { maximize, isCollapse, tabs, footer } = storeToRefs(globalStore);

watch(
  () => [maximize, isCollapse, tabs, footer],
  () => {
    debouncedResize();
  },
  { deep: true }
);

onMounted(() => {
  nextTick(() => init());
  window.addEventListener("resize", debouncedResize);
});

onActivated(() => {
  if (chartInstance.value) {
    chartInstance.value.resize();
  }
});

onBeforeUnmount(() => {
  chartInstance.value?.dispose();
  window.removeEventListener("resize", debouncedResize);
});

defineExpose({
  getInstance: () => chartInstance.value,
  resize,
  draw
});
</script>


================================================
FILE: src/components/ErrorMessage/403.vue
================================================
<template>
  <div class="not-container">
    <img src="@/assets/images/403.png" class="not-img" alt="403" />
    <div class="not-detail">
      <h2>403</h2>
      <h4>抱歉,您无权访问该页面~🙅‍♂️🙅‍♀️</h4>
      <el-button type="primary" @click="router.back"> 返回上一页 </el-button>
    </div>
  </div>
</template>

<script setup lang="ts" name="403">
import { useRouter } from "vue-router";
const router = useRouter();
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/ErrorMessage/404.vue
================================================
<template>
  <div class="not-container">
    <img src="@/assets/images/404.png" class="not-img" alt="404" />
    <div class="not-detail">
      <h2>404</h2>
      <h4>抱歉,您访问的页面不存在~🤷‍♂️🤷‍♀️</h4>
      <el-button type="primary" @click="router.back"> 返回上一页 </el-button>
    </div>
  </div>
</template>

<script setup lang="ts" name="404">
import { useRouter } from "vue-router";
const router = useRouter();
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/ErrorMessage/500.vue
================================================
<template>
  <div class="not-container">
    <img src="@/assets/images/500.png" class="not-img" alt="500" />
    <div class="not-detail">
      <h2>500</h2>
      <h4>抱歉,您的网络不见了~🤦‍♂️🤦‍♀️</h4>
      <el-button type="primary" @click="router.back"> 返回上一页 </el-button>
    </div>
  </div>
</template>

<script setup lang="ts" name="500">
import { useRouter } from "vue-router";
const router = useRouter();
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/ErrorMessage/index.scss
================================================
.not-container {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  .not-img {
    margin-right: 120px;
  }
  .not-detail {
    display: flex;
    flex-direction: column;
    h2,
    h4 {
      padding: 0;
      margin: 0;
    }
    h2 {
      font-size: 60px;
      color: var(--el-text-color-primary);
    }
    h4 {
      margin: 30px 0 20px;
      font-size: 19px;
      font-weight: normal;
      color: var(--el-text-color-regular);
    }
    .el-button {
      width: 100px;
    }
  }
}


================================================
FILE: src/components/Grid/components/GridItem.vue
================================================
<template>
  <div v-show="isShow" :style="style">
    <slot></slot>
  </div>
</template>
<script setup lang="ts" name="GridItem">
import { computed, inject, Ref, ref, useAttrs, watch } from "vue";
import { BreakPoint, Responsive } from "../interface/index";

type Props = {
  offset?: number;
  span?: number;
  suffix?: boolean;
  xs?: Responsive;
  sm?: Responsive;
  md?: Responsive;
  lg?: Responsive;
  xl?: Responsive;
};

const props = withDefaults(defineProps<Props>(), {
  offset: 0,
  span: 1,
  suffix: false,
  xs: undefined,
  sm: undefined,
  md: undefined,
  lg: undefined,
  xl: undefined
});

const attrs = useAttrs() as { index: string };
const isShow = ref(true);

// 注入断点
const breakPoint = inject<Ref<BreakPoint>>("breakPoint", ref("xl"));
const shouldHiddenIndex = inject<Ref<number>>("shouldHiddenIndex", ref(-1));
watch(
  () => [shouldHiddenIndex.value, breakPoint.value],
  n => {
    if (!!attrs.index) {
      isShow.value = !(n[0] !== -1 && parseInt(attrs.index) >= Number(n[0]));
    }
  },
  { immediate: true }
);

const gap = inject("gap", 0);
const cols = inject("cols", ref(4));
const style = computed(() => {
  let span = props[breakPoint.value]?.span ?? props.span;
  let offset = props[breakPoint.value]?.offset ?? props.offset;
  if (props.suffix) {
    return {
      gridColumnStart: cols.value - span - offset + 1,
      gridColumnEnd: `span ${span + offset}`,
      marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : "unset"
    };
  } else {
    return {
      gridColumn: `span ${span + offset > cols.value ? cols.value : span + offset}/span ${
        span + offset > cols.value ? cols.value : span + offset
      }`,
      marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : "unset"
    };
  }
});
</script>


================================================
FILE: src/components/Grid/index.vue
================================================
<template>
  <div :style="style">
    <slot></slot>
  </div>
</template>

<script setup lang="ts" name="Grid">
import {
  ref,
  watch,
  useSlots,
  computed,
  provide,
  onBeforeMount,
  onMounted,
  onUnmounted,
  onDeactivated,
  onActivated,
  VNodeArrayChildren,
  VNode
} from "vue";
import type { BreakPoint } from "./interface/index";

type Props = {
  cols?: number | Record<BreakPoint, number>;
  collapsed?: boolean;
  collapsedRows?: number;
  gap?: [number, number] | number;
};

const props = withDefaults(defineProps<Props>(), {
  cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
  collapsed: false,
  collapsedRows: 1,
  gap: 0
});

onBeforeMount(() => props.collapsed && findIndex());
onMounted(() => {
  resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  window.addEventListener("resize", resize);
});
onActivated(() => {
  resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  window.addEventListener("resize", resize);
});
onUnmounted(() => {
  window.removeEventListener("resize", resize);
});
onDeactivated(() => {
  window.removeEventListener("resize", resize);
});

// 监听屏幕变化
const resize = (e: UIEvent) => {
  let width = (e.target as Window).innerWidth;
  switch (!!width) {
    case width < 768:
      breakPoint.value = "xs";
      break;
    case width >= 768 && width < 992:
      breakPoint.value = "sm";
      break;
    case width >= 992 && width < 1200:
      breakPoint.value = "md";
      break;
    case width >= 1200 && width < 1920:
      breakPoint.value = "lg";
      break;
    case width >= 1920:
      breakPoint.value = "xl";
      break;
  }
};

// 注入 gap 间距
provide("gap", Array.isArray(props.gap) ? props.gap[0] : props.gap);

// 注入响应式断点
let breakPoint = ref<BreakPoint>("xl");
provide("breakPoint", breakPoint);

// 注入要开始折叠的 index
const hiddenIndex = ref(-1);
provide("shouldHiddenIndex", hiddenIndex);

// 注入 cols
const gridCols = computed(() => {
  if (typeof props.cols === "object") return props.cols[breakPoint.value] ?? props.cols;
  return props.cols;
});
provide("cols", gridCols);

// 寻找需要开始折叠的字段 index
const slots = useSlots().default!();

const findIndex = () => {
  let fields: VNodeArrayChildren = [];
  let suffix: VNode | null = null;
  slots.forEach((slot: any) => {
    // suffix
    if (typeof slot.type === "object" && slot.type.name === "GridItem" && slot.props?.suffix !== undefined) suffix = slot;
    // slot children
    if (typeof slot.type === "symbol" && Array.isArray(slot.children)) fields.push(...slot.children);
  });

  // 计算 suffix 所占用的列
  let suffixCols = 0;
  if (suffix) {
    suffixCols =
      ((suffix as VNode).props![breakPoint.value]?.span ?? (suffix as VNode).props?.span ?? 1) +
      ((suffix as VNode).props![breakPoint.value]?.offset ?? (suffix as VNode).props?.offset ?? 0);
  }
  try {
    let find = false;
    fields.reduce((prev = 0, current, index) => {
      prev +=
        ((current as VNode)!.props![breakPoint.value]?.span ?? (current as VNode)!.props?.span ?? 1) +
        ((current as VNode)!.props![breakPoint.value]?.offset ?? (current as VNode)!.props?.offset ?? 0);
      if (Number(prev) > props.collapsedRows * gridCols.value - suffixCols) {
        hiddenIndex.value = index;
        find = true;
        throw "find it";
      }
      return prev;
    }, 0);
    if (!find) hiddenIndex.value = -1;
  } catch (e) {
    // console.warn(e);
  }
};

// 断点变化时执行 findIndex
watch(
  () => breakPoint.value,
  () => {
    if (props.collapsed) findIndex();
  }
);

// 监听 collapsed
watch(
  () => props.collapsed,
  value => {
    if (value) return findIndex();
    hiddenIndex.value = -1;
  }
);

// 设置间距
const gridGap = computed(() => {
  if (typeof props.gap === "number") return `${props.gap}px`;
  if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`;
  return "unset";
});

// 设置 style
const style = computed(() => {
  return {
    display: "grid",
    gridGap: gridGap.value,
    gridTemplateColumns: `repeat(${gridCols.value}, minmax(0, 1fr))`
  };
});

defineExpose({ breakPoint });
</script>


================================================
FILE: src/components/Grid/interface/index.ts
================================================
export type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl";

export type Responsive = {
  span?: number;
  offset?: number;
};


================================================
FILE: src/components/ImportExcel/index.scss
================================================
.upload {
  width: 80%;
}


================================================
FILE: src/components/ImportExcel/index.vue
================================================
<template>
  <el-dialog v-model="dialogVisible" :title="`批量添加${parameter.title}`" :destroy-on-close="true" width="580px" draggable>
    <el-form class="drawer-multiColumn-form" label-width="100px">
      <el-form-item label="模板下载 :">
        <el-button type="primary" :icon="Download" @click="downloadTemp"> 点击下载 </el-button>
      </el-form-item>
      <el-form-item label="文件上传 :">
        <el-upload
          action="#"
          class="upload"
          :drag="true"
          :limit="excelLimit"
          :multiple="true"
          :show-file-list="true"
          :http-request="uploadExcel"
          :before-upload="beforeExcelUpload"
          :on-exceed="handleExceed"
          :on-success="excelUploadSuccess"
          :on-error="excelUploadError"
          :accept="parameter.fileType!.join(',')"
        >
          <slot name="empty">
            <el-icon class="el-icon--upload">
              <upload-filled />
            </el-icon>
            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
          </slot>
          <template #tip>
            <slot name="tip">
              <div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件,文件最大为 {{ parameter.fileSize }}M</div>
            </slot>
          </template>
        </el-upload>
      </el-form-item>
      <el-form-item label="数据覆盖 :">
        <el-switch v-model="isCover" />
      </el-form-item>
    </el-form>
  </el-dialog>
</template>

<script setup lang="ts" name="ImportExcel">
import { ref } from "vue";
import { useDownload } from "@/hooks/useDownload";
import { Download } from "@element-plus/icons-vue";
import { ElNotification, UploadRequestOptions, UploadRawFile } from "element-plus";

export interface ExcelParameterProps {
  title: string; // 标题
  fileSize?: number; // 上传文件的大小
  fileType?: File.ExcelMimeType[]; // 上传文件的类型
  tempApi?: (params: any) => Promise<any>; // 下载模板的Api
  importApi?: (params: any) => Promise<any>; // 批量导入的Api
  getTableList?: () => void; // 获取表格数据的Api
}

// 是否覆盖数据
const isCover = ref(false);
// 最大文件上传数
const excelLimit = ref(1);
// dialog状态
const dialogVisible = ref(false);
// 父组件传过来的参数
const parameter = ref<ExcelParameterProps>({
  title: "",
  fileSize: 5,
  fileType: ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
});

// 接收父组件参数
const acceptParams = (params: ExcelParameterProps) => {
  parameter.value = { ...parameter.value, ...params };
  dialogVisible.value = true;
};

// Excel 导入模板下载
const downloadTemp = () => {
  if (!parameter.value.tempApi) return;
  useDownload(parameter.value.tempApi, `${parameter.value.title}模板`);
};

// 文件上传
const uploadExcel = async (param: UploadRequestOptions) => {
  let excelFormData = new FormData();
  excelFormData.append("file", param.file);
  excelFormData.append("isCover", isCover.value as unknown as Blob);
  await parameter.value.importApi!(excelFormData);
  parameter.value.getTableList && parameter.value.getTableList();
  dialogVisible.value = false;
};

/**
 * @description 文件上传之前判断
 * @param file 上传的文件
 * */
const beforeExcelUpload = (file: UploadRawFile) => {
  const isExcel = parameter.value.fileType!.includes(file.type as File.ExcelMimeType);
  const fileSize = file.size / 1024 / 1024 < parameter.value.fileSize!;
  if (!isExcel)
    ElNotification({
      title: "温馨提示",
      message: "上传文件只能是 xls / xlsx 格式!",
      type: "warning"
    });
  if (!fileSize)
    setTimeout(() => {
      ElNotification({
        title: "温馨提示",
        message: `上传文件大小不能超过 ${parameter.value.fileSize}MB!`,
        type: "warning"
      });
    }, 0);
  return isExcel && fileSize;
};

// 文件数超出提示
const handleExceed = () => {
  ElNotification({
    title: "温馨提示",
    message: "最多只能上传一个文件!",
    type: "warning"
  });
};

// 上传错误提示
const excelUploadError = () => {
  ElNotification({
    title: "温馨提示",
    message: `批量添加${parameter.value.title}失败,请您重新上传!`,
    type: "error"
  });
};

// 上传成功提示
const excelUploadSuccess = () => {
  ElNotification({
    title: "温馨提示",
    message: `批量添加${parameter.value.title}成功!`,
    type: "success"
  });
};

defineExpose({
  acceptParams
});
</script>
<style lang="scss" scoped>
@import "./index.scss";
</style>


================================================
FILE: src/components/Loading/fullScreen.ts
================================================
import { ElLoading } from "element-plus";

/* 全局请求 loading */
let loadingInstance: ReturnType<typeof ElLoading.service>;

/**
 * @description 开启 Loading
 * */
const startLoading = () => {
  loadingInstance = ElLoading.service({
    fullscreen: true,
    lock: true,
    text: "Loading",
    background: "rgba(0, 0, 0, 0.7)"
  });
};

/**
 * @description 结束 Loading
 * */
const endLoading = () => {
  loadingInstance.close();
};

/**
 * @description 显示全屏加载
 * */
let needLoadingRequestCount = 0;
export const showFullScreenLoading = () => {
  if (needLoadingRequestCount === 0) {
    startLoading();
  }
  needLoadingRequestCount++;
};

/**
 * @description 隐藏全屏加载
 * */
export const tryHideFullScreenLoading = () => {
  if (needLoadingRequestCount <= 0) return;
  needLoadingRequestCount--;
  if (needLoadingRequestCount === 0) {
    endLoading();
  }
};


================================================
FILE: src/components/Loading/index.scss
================================================
.loading-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  .loading-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 98px;
  }
}
.dot {
  position: relative;
  box-sizing: border-box;
  display: inline-block;
  width: 32px;
  height: 32px;
  font-size: 32px;
  transform: rotate(45deg);
  animation: ant-rotate 1.2s infinite linear;
}
.dot i {
  position: absolute;
  display: block;
  width: 14px;
  height: 14px;
  background-color: var(--el-color-primary);
  border-radius: 100%;
  opacity: 0.3;
  transform: scale(0.75);
  transform-origin: 50% 50%;
  animation: ant-spin-move 1s infinite linear alternate;
}
.dot i:nth-child(1) {
  top: 0;
  left: 0;
}
.dot i:nth-child(2) {
  top: 0;
  right: 0;
  animation-delay: 0.4s;
}
.dot i:nth-child(3) {
  right: 0;
  bottom: 0;
  animation-delay: 0.8s;
}
.dot i:nth-child(4) {
  bottom: 0;
  left: 0;
  animation-delay: 1.2s;
}

@keyframes ant-rotate {
  to {
    transform: rotate(405deg);
  }
}

@keyframes ant-spin-move {
  to {
    opacity: 1;
  }
}


================================================
FILE: src/components/Loading/index.vue
================================================
<template>
  <div class="loading-box">
    <div class="loading-wrap">
      <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
    </div>
  </div>
</template>

<script setup lang="ts" name="Loading"></script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/ProTable/components/ColSetting.vue
================================================
<template>
  <!-- 列设置 -->
  <el-drawer v-model="drawerVisible" title="列设置" size="450px">
    <div class="table-main">
      <el-table :data="colSetting" :border="true" row-key="prop" default-expand-all :tree-props="{ children: '_children' }">
        <el-table-column prop="label" align="center" label="列名" />
        <el-table-column v-slot="scope" prop="isShow" align="center" label="显示">
          <el-switch v-model="scope.row.isShow"></el-switch>
        </el-table-column>
        <el-table-column v-slot="scope" prop="sortable" align="center" label="排序">
          <el-switch v-model="scope.row.sortable"></el-switch>
        </el-table-column>
        <template #empty>
          <div class="table-empty">
            <img src="@/assets/images/notData.png" alt="notData" />
            <div>暂无可配置列</div>
          </div>
        </template>
      </el-table>
    </div>
  </el-drawer>
</template>

<script setup lang="ts" name="ColSetting">
import { ref } from "vue";
import { ColumnProps } from "@/components/ProTable/interface";

defineProps<{ colSetting: ColumnProps[] }>();

const drawerVisible = ref<boolean>(false);

const openColSetting = () => {
  drawerVisible.value = true;
};

defineExpose({
  openColSetting
});
</script>

<style scoped lang="scss">
.cursor-move {
  cursor: move;
}
</style>


================================================
FILE: src/components/ProTable/components/Pagination.vue
================================================
<template>
  <!-- 分页组件 -->
  <el-pagination
    :background="true"
    :current-page="pageable.pageNum"
    :page-size="pageable.pageSize"
    :page-sizes="[10, 25, 50, 100]"
    :total="pageable.total"
    :size="globalStore?.assemblySize ?? 'default'"
    layout="total, sizes, prev, pager, next, jumper"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
  ></el-pagination>
</template>

<script setup lang="ts" name="Pagination">
import { useGlobalStore } from "@/stores/modules/global";
const globalStore = useGlobalStore();

interface Pageable {
  pageNum: number;
  pageSize: number;
  total: number;
}

interface PaginationProps {
  pageable: Pageable;
  handleSizeChange: (size: number) => void;
  handleCurrentChange: (currentPage: number) => void;
}

defineProps<PaginationProps>();
</script>


================================================
FILE: src/components/ProTable/components/TableColumn.vue
================================================
<template>
  <RenderTableColumn v-bind="column" />
</template>

<script setup lang="tsx" name="TableColumn">
import { inject, ref, useSlots } from "vue";
import { ColumnProps, RenderScope, HeaderRenderScope } from "@/components/ProTable/interface";
import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from "@/utils";

defineProps<{ column: ColumnProps }>();

const slots = useSlots();

const enumMap = inject("enumMap", ref(new Map()));

// 渲染表格数据
const renderCellData = (item: ColumnProps, scope: RenderScope<any>) => {
  return enumMap.value.get(item.prop) && item.isFilterEnum
    ? filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop)!, item.fieldNames)
    : formatValue(handleRowAccordingToProp(scope.row, item.prop!));
};

// 获取 tag 类型
const getTagType = (item: ColumnProps, scope: RenderScope<any>) => {
  return (
    filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames, "tag") || "primary"
  );
};

const RenderTableColumn = (item: ColumnProps) => {
  return (
    <>
      {item.isShow && (
        <el-table-column
          {...item}
          align={item.align ?? "center"}
          showOverflowTooltip={item.showOverflowTooltip ?? item.prop !== "operation"}
        >
          {{
            default: (scope: RenderScope<any>) => {
              if (item._children) return item._children.map(child => RenderTableColumn(child));
              if (item.render) return item.render(scope);
              if (item.prop && slots[handleProp(item.prop)]) return slots[handleProp(item.prop)]!(scope);
              if (item.tag) return <el-tag type={getTagType(item, scope)}>{renderCellData(item, scope)}</el-tag>;
              return renderCellData(item, scope);
            },
            header: (scope: HeaderRenderScope<any>) => {
              if (item.headerRender) return item.headerRender(scope);
              if (item.prop && slots[`${handleProp(item.prop)}Header`]) return slots[`${handleProp(item.prop)}Header`]!(scope);
              return item.label;
            }
          }}
        </el-table-column>
      )}
    </>
  );
};
</script>


================================================
FILE: src/components/ProTable/index.vue
================================================
<!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 -->

<template>
  <!-- 查询表单 -->
  <SearchForm
    v-show="isShowSearch"
    :search="_search"
    :reset="_reset"
    :columns="searchColumns"
    :search-param="searchParam"
    :search-col="searchCol"
  />

  <!-- 表格主体 -->
  <div class="card table-main">
    <!-- 表格头部 操作按钮 -->
    <div class="table-header">
      <div class="header-button-lf">
        <slot name="tableHeader" :selected-list="selectedList" :selected-list-ids="selectedListIds" :is-selected="isSelected" />
      </div>
      <div v-if="toolButton" class="header-button-ri">
        <slot name="toolButton">
          <el-button v-if="showToolButton('refresh')" :icon="Refresh" circle @click="getTableList" />
          <el-button v-if="showToolButton('setting') && columns.length" :icon="Operation" circle @click="openColSetting" />
          <el-button
            v-if="showToolButton('search') && searchColumns?.length"
            :icon="Search"
            circle
            @click="isShowSearch = !isShowSearch"
          />
        </slot>
      </div>
    </div>
    <!-- 表格主体 -->
    <el-table
      ref="tableRef"
      v-bind="$attrs"
      :id="uuid"
      :data="processTableData"
      :border="border"
      :row-key="rowKey"
      @selection-change="selectionChange"
    >
      <!-- 默认插槽 -->
      <slot />
      <template v-for="item in tableColumns" :key="item">
        <!-- selection || radio || index || expand || sort -->
        <el-table-column
          v-if="item.type && columnTypes.includes(item.type)"
          v-bind="item"
          :align="item.align ?? 'center'"
          :reserve-selection="item.type == 'selection'"
        >
          <template #default="scope">
            <!-- expand -->
            <template v-if="item.type == 'expand'">
              <component :is="item.render" v-bind="scope" v-if="item.render" />
              <slot v-else :name="item.type" v-bind="scope" />
            </template>
            <!-- radio -->
            <el-radio v-if="item.type == 'radio'" v-model="radio" :label="scope.row[rowKey]">
              <i></i>
            </el-radio>
            <!-- sort -->
            <el-tag v-if="item.type == 'sort'" class="move">
              <el-icon> <DCaret /></el-icon>
            </el-tag>
          </template>
        </el-table-column>
        <!-- other -->
        <TableColumn v-else :column="item">
          <template v-for="slot in Object.keys($slots)" #[slot]="scope">
            <slot :name="slot" v-bind="scope" />
          </template>
        </TableColumn>
      </template>
      <!-- 插入表格最后一行之后的插槽 -->
      <template #append>
        <slot name="append" />
      </template>
      <!-- 无数据 -->
      <template #empty>
        <div class="table-empty">
          <slot name="empty">
            <img src="@/assets/images/notData.png" alt="notData" />
            <div>暂无数据</div>
          </slot>
        </div>
      </template>
    </el-table>
    <!-- 分页组件 -->
    <slot name="pagination">
      <Pagination
        v-if="pagination"
        :pageable="pageable"
        :handle-size-change="handleSizeChange"
        :handle-current-change="handleCurrentChange"
      />
    </slot>
  </div>
  <!-- 列设置 -->
  <ColSetting v-if="toolButton" ref="colRef" v-model:col-setting="colSetting" />
</template>

<script setup lang="ts" name="ProTable">
import { ref, watch, provide, onMounted, unref, computed, reactive } from "vue";
import { ElTable } from "element-plus";
import { useTable } from "@/hooks/useTable";
import { useSelection } from "@/hooks/useSelection";
import { BreakPoint } from "@/components/Grid/interface";
import { ColumnProps, TypeProps } from "@/components/ProTable/interface";
import { Refresh, Operation, Search } from "@element-plus/icons-vue";
import { generateUUID, handleProp } from "@/utils";
import SearchForm from "@/components/SearchForm/index.vue";
import Pagination from "./components/Pagination.vue";
import ColSetting from "./components/ColSetting.vue";
import TableColumn from "./components/TableColumn.vue";
import Sortable from "sortablejs";

export interface ProTableProps {
  columns: ColumnProps[]; // 列配置项  ==> 必传
  data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
  requestApi?: (params: any) => Promise<any>; // 请求表格数据的 api ==> 非必传
  requestAuto?: boolean; // 是否自动执行请求 api ==> 非必传(默认为true)
  requestError?: (params: any) => void; // 表格 api 请求错误监听 ==> 非必传
  dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理 ==> 非必传
  title?: string; // 表格标题 ==> 非必传
  pagination?: boolean; // 是否需要分页组件 ==> 非必传(默认为true)
  initParam?: any; // 初始化请求参数 ==> 非必传(默认为{})
  border?: boolean; // 是否带有纵向边框 ==> 非必传(默认为true)
  toolButton?: ("refresh" | "setting" | "search")[] | boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true)
  rowKey?: string; // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ==> 非必传(默认为 id)
  searchCol?: number | Record<BreakPoint, number>; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
}

// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<ProTableProps>(), {
  columns: () => [],
  requestAuto: true,
  pagination: true,
  initParam: {},
  border: true,
  toolButton: true,
  rowKey: "id",
  searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 })
});

// table 实例
const tableRef = ref<InstanceType<typeof ElTable>>();

// 生成组件唯一id
const uuid = ref("id-" + generateUUID());

// column 列类型
const columnTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"];

// 是否显示搜索模块
const isShowSearch = ref(true);

// 控制 ToolButton 显示
const showToolButton = (key: "refresh" | "setting" | "search") => {
  return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
};

// 单选值
const radio = ref("");

// 表格多选 Hooks
const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey);

// 表格操作 Hooks
const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange } =
  useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback, props.requestError);

// 清空选中数据列表
const clearSelection = () => tableRef.value!.clearSelection();

// 初始化表格数据 && 拖拽排序
onMounted(() => {
  dragSort();
  props.requestAuto && getTableList();
  props.data && (pageable.value.total = props.data.length);
});

// 处理表格数据
const processTableData = computed(() => {
  if (!props.data) return tableData.value;
  if (!props.pagination) return props.data;
  return props.data.slice(
    (pageable.value.pageNum - 1) * pageable.value.pageSize,
    pageable.value.pageSize * pageable.value.pageNum
  );
});

// 监听页面 initParam 改化,重新获取表格数据
watch(() => props.initParam, getTableList, { deep: true });

// 接收 columns 并设置为响应式
const tableColumns = reactive<ColumnProps[]>(props.columns);

// 扁平化 columns
const flatColumns = computed(() => flatColumnsFunc(tableColumns));

// 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
const enumMap = ref(new Map<string, { [key: string]: any }[]>());
const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => {
  if (!enumValue) return;

  // 如果当前 enumMap 存在相同的值 return
  if (enumMap.value.has(prop!) && (typeof enumValue === "function" || enumMap.value.get(prop!) === enumValue)) return;

  // 当前 enum 为静态数据,则直接存储到 enumMap
  if (typeof enumValue !== "function") return enumMap.value.set(prop!, unref(enumValue!));

  // 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储
  enumMap.value.set(prop!, []);

  // 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap
  const { data } = await enumValue();
  enumMap.value.set(prop!, data);
};

// 注入 enumMap
provide("enumMap", enumMap);

// 扁平化 columns 的方法
const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
  columns.forEach(async col => {
    if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children));
    flatArr.push(col);

    // column 添加默认 isShow && isSetting && isFilterEnum 属性值
    col.isShow = col.isShow ?? true;
    col.isSetting = col.isSetting ?? true;
    col.isFilterEnum = col.isFilterEnum ?? true;

    // 设置 enumMap
    await setEnumMap(col);
  });
  return flatArr.filter(item => !item._children?.length);
};

// 过滤需要搜索的配置项 && 排序
const searchColumns = computed(() => {
  return flatColumns.value
    ?.filter(item => item.search?.el || item.search?.render)
    .sort((a, b) => a.search!.order! - b.search!.order!);
});

// 设置 搜索表单默认排序 && 搜索表单项的默认值
searchColumns.value?.forEach((column, index) => {
  column.search!.order = column.search?.order ?? index + 2;
  const key = column.search?.key ?? handleProp(column.prop!);
  const defaultValue = column.search?.defaultValue;
  if (defaultValue !== undefined && defaultValue !== null) {
    searchParam.value[key] = defaultValue;
    searchInitParam.value[key] = defaultValue;
  }
});

// 列设置 ==> 需要过滤掉不需要设置的列
const colRef = ref();
const colSetting = tableColumns!.filter(item => {
  const { type, prop, isSetting } = item;
  return !columnTypes.includes(type!) && prop !== "operation" && isSetting;
});
const openColSetting = () => colRef.value.openColSetting();

// 定义 emit 事件
const emit = defineEmits<{
  search: [];
  reset: [];
  dragSort: [{ newIndex?: number; oldIndex?: number }];
}>();

const _search = () => {
  search();
  emit("search");
};

const _reset = () => {
  reset();
  emit("reset");
};

// 表格拖拽排序
const dragSort = () => {
  const tbody = document.querySelector(`#${uuid.value} tbody`) as HTMLElement;
  Sortable.create(tbody, {
    handle: ".move",
    animation: 300,
    onEnd({ newIndex, oldIndex }) {
      const [removedItem] = processTableData.value.splice(oldIndex!, 1);
      processTableData.value.splice(newIndex!, 0, removedItem);
      emit("dragSort", { newIndex, oldIndex });
    }
  });
};

// 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去)
defineExpose({
  element: tableRef,
  tableData: processTableData,
  radio,
  pageable,
  searchParam,
  searchInitParam,
  isSelected,
  selectedList,
  selectedListIds,

  // 下面为 function
  getTableList,
  search,
  reset,
  handleSizeChange,
  handleCurrentChange,
  clearSelection,
  enumMap
});
</script>


================================================
FILE: src/components/ProTable/interface/index.ts
================================================
import { VNode, ComponentPublicInstance, Ref } from "vue";
import { BreakPoint, Responsive } from "@/components/Grid/interface";
import { TableColumnCtx } from "element-plus/es/components/table/src/table-column/defaults";
import { ProTableProps } from "@/components/ProTable/index.vue";
import ProTable from "@/components/ProTable/index.vue";

export interface EnumProps {
  label?: string; // 选项框显示的文字
  value?: string | number | boolean | any[]; // 选项框值
  disabled?: boolean; // 是否禁用此选项
  tagType?: string; // 当 tag 为 true 时,此选择会指定 tag 显示类型
  children?: EnumProps[]; // 为树形选择时,可以通过 children 属性指定子选项
  [key: string]: any;
}

export type TypeProps = "index" | "selection" | "radio" | "expand" | "sort";

export type SearchType =
  | "input"
  | "input-number"
  | "select"
  | "select-v2"
  | "tree-select"
  | "cascader"
  | "date-picker"
  | "time-picker"
  | "time-select"
  | "switch"
  | "slider";

export type SearchRenderScope = {
  searchParam: { [key: string]: any };
  placeholder: string;
  clearable: boolean;
  options: EnumProps[];
  data: EnumProps[];
};

export type SearchProps = {
  el?: SearchType; // 当前项搜索框的类型
  label?: string; // 当前项搜索框的 label
  props?: any; // 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件
  key?: string; // 当搜索项 key 不为 prop 属性时,可通过 key 指定
  tooltip?: string; // 搜索提示
  order?: number; // 搜索项排序(从大到小)
  span?: number; // 搜索项所占用的列数,默认为 1 列
  offset?: number; // 搜索字段左侧偏移列数
  defaultValue?: string | number | boolean | any[] | Ref<any>; // 搜索项默认值
  render?: (scope: SearchRenderScope) => VNode; // 自定义搜索内容渲染(tsx语法)
} & Partial<Record<BreakPoint, Responsive>>;

export type FieldNamesProps = {
  label: string;
  value: string;
  children?: string;
};

export type RenderScope<T> = {
  row: T;
  $index: number;
  column: TableColumnCtx<T>;
  [key: string]: any;
};

export type HeaderRenderScope<T> = {
  $index: number;
  column: TableColumnCtx<T>;
  [key: string]: any;
};

export interface ColumnProps<T = any>
  extends Partial<Omit<TableColumnCtx<T>, "type" | "children" | "renderCell" | "renderHeader">> {
  type?: TypeProps; // 列类型
  tag?: boolean | Ref<boolean>; // 是否是标签展示
  isShow?: boolean | Ref<boolean>; // 是否显示在表格当中
  isSetting?: boolean | Ref<boolean>; // 是否在 ColSetting 中可配置
  search?: SearchProps | undefined; // 搜索项配置
  enum?: EnumProps[] | Ref<EnumProps[]> | ((params?: any) => Promise<any>); // 枚举字典
  isFilterEnum?: boolean | Ref<boolean>; // 当前单元格值是否根据 enum 格式化(示例:enum 只作为搜索项数据)
  fieldNames?: FieldNamesProps; // 指定 label && value && children 的 key 值
  headerRender?: (scope: HeaderRenderScope<T>) => VNode; // 自定义表头内容渲染(tsx语法)
  render?: (scope: RenderScope<T>) => VNode | string; // 自定义单元格内容渲染(tsx语法)
  _children?: ColumnProps<T>[]; // 多级表头
}

export type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof ComponentPublicInstance | keyof ProTableProps>;


================================================
FILE: src/components/SearchForm/components/SearchFormItem.vue
================================================
<template>
  <component
    :is="column.search?.render ?? `el-${column.search?.el}`"
    v-bind="{ ...handleSearchProps, ...placeholder, searchParam: _searchParam, clearable }"
    v-model.trim="_searchParam[column.search?.key ?? handleProp(column.prop!)]"
    :data="column.search?.el === 'tree-select' ? columnEnum : []"
    :options="['cascader', 'select-v2'].includes(column.search?.el!) ? columnEnum : []"
  >
    <template v-if="column.search?.el === 'cascader'" #default="{ data }">
      <span>{{ data[fieldNames.label] }}</span>
    </template>
    <template v-if="column.search?.el === 'select'">
      <component
        :is="`el-option`"
        v-for="(col, index) in columnEnum"
        :key="index"
        :label="col[fieldNames.label]"
        :value="col[fieldNames.value]"
      ></component>
    </template>
    <slot v-else></slot>
  </component>
</template>

<script setup lang="ts" name="SearchFormItem">
import { computed, inject, ref } from "vue";
import { handleProp } from "@/utils";
import { ColumnProps } from "@/components/ProTable/interface";

interface SearchFormItem {
  column: ColumnProps;
  searchParam: { [key: string]: any };
}
const props = defineProps<SearchFormItem>();

// Re receive SearchParam
const _searchParam = computed(() => props.searchParam);

// 判断 fieldNames 设置 label && value && children 的 key 值
const fieldNames = computed(() => {
  return {
    label: props.column.fieldNames?.label ?? "label",
    value: props.column.fieldNames?.value ?? "value",
    children: props.column.fieldNames?.children ?? "children"
  };
});

// 接收 enumMap (el 为 select-v2 需单独处理 enumData)
const enumMap = inject("enumMap", ref(new Map()));
const columnEnum = computed(() => {
  let enumData = enumMap.value.get(props.column.prop);
  if (!enumData) return [];
  if (props.column.search?.el === "select-v2" && props.column.fieldNames) {
    enumData = enumData.map((item: { [key: string]: any }) => {
      return { ...item, label: item[fieldNames.value.label], value: item[fieldNames.value.value] };
    });
  }
  return enumData;
});

// 处理透传的 searchProps (el 为 tree-select、cascader 的时候需要给下默认 label && value && children)
const handleSearchProps = computed(() => {
  const label = fieldNames.value.label;
  const value = fieldNames.value.value;
  const children = fieldNames.value.children;
  const searchEl = props.column.search?.el;
  let searchProps = props.column.search?.props ?? {};
  if (searchEl === "tree-select") {
    searchProps = { ...searchProps, props: { ...searchProps, label, children }, nodeKey: value };
  }
  if (searchEl === "cascader") {
    searchProps = { ...searchProps, props: { ...searchProps, label, value, children } };
  }
  return searchProps;
});

// 处理默认 placeholder
const placeholder = computed(() => {
  const search = props.column.search;
  if (["datetimerange", "daterange", "monthrange"].includes(search?.props?.type) || search?.props?.isRange) {
    return {
      rangeSeparator: search?.props?.rangeSeparator ?? "至",
      startPlaceholder: search?.props?.startPlaceholder ?? "开始时间",
      endPlaceholder: search?.props?.endPlaceholder ?? "结束时间"
    };
  }
  const placeholder = search?.props?.placeholder ?? (search?.el?.includes("input") ? "请输入" : "请选择");
  return { placeholder };
});

// 是否有清除按钮 (当搜索项有默认值时,清除按钮不显示)
const clearable = computed(() => {
  const search = props.column.search;
  return search?.props?.clearable ?? (search?.defaultValue == null || search?.defaultValue == undefined);
});
</script>


================================================
FILE: src/components/SearchForm/index.vue
================================================
<template>
  <div v-if="columns.length" class="card table-search">
    <el-form ref="formRef" :model="searchParam">
      <Grid ref="gridRef" :collapsed="collapsed" :gap="[20, 0]" :cols="searchCol">
        <GridItem v-for="(item, index) in columns" :key="item.prop" v-bind="getResponsive(item)" :index="index">
          <el-form-item>
            <template #label>
              <el-space :size="4">
                <span>{{ `${item.search?.label ?? item.label}` }}</span>
                <el-tooltip v-if="item.search?.tooltip" effect="dark" :content="item.search?.tooltip" placement="top">
                  <i :class="'iconfont icon-yiwen'"></i>
                </el-tooltip>
              </el-space>
              <span>&nbsp;:</span>
            </template>
            <SearchFormItem :column="item" :search-param="searchParam" />
          </el-form-item>
        </GridItem>
        <GridItem suffix>
          <div class="operation">
            <el-button type="primary" :icon="Search" @click="search"> 搜索 </el-button>
            <el-button :icon="Delete" @click="reset"> 重置 </el-button>
            <el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
              {{ collapsed ? "展开" : "合并" }}
              <el-icon class="el-icon--right">
                <component :is="collapsed ? ArrowDown : ArrowUp"></component>
              </el-icon>
            </el-button>
          </div>
        </GridItem>
      </Grid>
    </el-form>
  </div>
</template>
<script setup lang="ts" name="SearchForm">
import { computed, ref } from "vue";
import { ColumnProps } from "@/components/ProTable/interface";
import { BreakPoint } from "@/components/Grid/interface";
import { Delete, Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import SearchFormItem from "./components/SearchFormItem.vue";
import Grid from "@/components/Grid/index.vue";
import GridItem from "@/components/Grid/components/GridItem.vue";

interface ProTableProps {
  columns?: ColumnProps[]; // 搜索配置列
  searchParam?: { [key: string]: any }; // 搜索参数
  searchCol: number | Record<BreakPoint, number>;
  search: (params: any) => void; // 搜索方法
  reset: (params: any) => void; // 重置方法
}

// 默认值
const props = withDefaults(defineProps<ProTableProps>(), {
  columns: () => [],
  searchParam: () => ({})
});

// 获取响应式设置
const getResponsive = (item: ColumnProps) => {
  return {
    span: item.search?.span,
    offset: item.search?.offset ?? 0,
    xs: item.search?.xs,
    sm: item.search?.sm,
    md: item.search?.md,
    lg: item.search?.lg,
    xl: item.search?.xl
  };
};

// 是否默认折叠搜索项
const collapsed = ref(true);

// 获取响应式断点
const gridRef = ref();
const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);

// 判断是否显示 展开/合并 按钮
const showCollapse = computed(() => {
  let show = false;
  props.columns.reduce((prev, current) => {
    prev +=
      (current.search![breakPoint.value]?.span ?? current.search?.span ?? 1) +
      (current.search![breakPoint.value]?.offset ?? current.search?.offset ?? 0);
    if (typeof props.searchCol !== "number") {
      if (prev >= props.searchCol[breakPoint.value]) show = true;
    } else {
      if (prev >= props.searchCol) show = true;
    }
    return prev;
  }, 0);
  return show;
});
</script>


================================================
FILE: src/components/SelectFilter/index.scss
================================================
.select-filter {
  width: 100%;
  .select-filter-item {
    display: flex;
    align-items: center;
    border-bottom: 1px dashed var(--el-border-color-light);
    &:last-child {
      border-bottom: none;
    }
    .select-filter-item-title {
      margin-top: -2px;
      span {
        font-size: 14px;
        color: var(--el-text-color-regular);
        white-space: nowrap;
      }
    }
    .select-filter-notData {
      margin: 18px 0;
      font-size: 14px;
      color: var(--el-text-color-regular);
    }
    .select-filter-list {
      display: flex;
      flex: 1;
      padding: 0;
      margin: 13px 0;
      li {
        display: flex;
        align-items: center;
        padding: 5px 15px;
        margin-right: 16px;
        font-size: 13px;
        color: var(--el-color-primary);
        list-style: none;
        cursor: pointer;
        background: var(--el-color-primary-light-9);
        border: 1px solid var(--el-color-primary-light-5);
        border-radius: 32px;
        &:hover {
          color: #ffffff;
          background: var(--el-color-primary);
          border-color: var(--el-color-primary);
          transition: 0.1s;
        }
        &.active {
          font-weight: bold;
          color: #ffffff;
          background: var(--el-color-primary);
          border-color: var(--el-color-primary);
        }
        .el-icon {
          margin-right: 4px;
          font-size: 16px;
          font-weight: bold;
        }
        span {
          white-space: nowrap;
        }
      }
    }
  }
}


================================================
FILE: src/components/SelectFilter/index.vue
================================================
<template>
  <div class="select-filter">
    <div v-for="item in data" :key="item.key" class="select-filter-item">
      <div class="select-filter-item-title">
        <span>{{ item.title }} :</span>
      </div>
      <span v-if="!item.options.length" class="select-filter-notData">暂无数据 ~</span>
      <el-scrollbar>
        <ul class="select-filter-list">
          <li
            v-for="option in item.options"
            :key="option.value"
            :class="{
              active:
                option.value === selected[item.key] ||
                (Array.isArray(selected[item.key]) && selected[item.key].includes(option.value))
            }"
            @click="select(item, option)"
          >
            <slot :row="option">
              <el-icon v-if="option.icon">
                <component :is="option.icon" />
              </el-icon>
              <span>{{ option.label }}</span>
            </slot>
          </li>
        </ul>
      </el-scrollbar>
    </div>
  </div>
</template>

<script setup lang="ts" name="selectFilter">
import { ref, watch } from "vue";

interface OptionsProps {
  value: string | number;
  label: string;
  icon?: string;
}

interface SelectDataProps {
  title: string; // 列表标题
  key: string; // 当前筛选项 key 值
  multiple?: boolean; // 是否为多选
  options: OptionsProps[]; // 筛选数据
}

interface SelectFilterProps {
  data?: SelectDataProps[]; // 选择的列表数据
  defaultValues?: { [key: string]: any }; // 默认值
}

const props = withDefaults(defineProps<SelectFilterProps>(), {
  data: () => [],
  defaultValues: () => ({})
});

// 重新接收默认值
const selected = ref<{ [key: string]: any }>({});
watch(
  () => props.defaultValues,
  () => {
    props.data.forEach(item => {
      if (item.multiple) selected.value[item.key] = props.defaultValues[item.key] ?? [""];
      else selected.value[item.key] = props.defaultValues[item.key] ?? "";
    });
  },
  { deep: true, immediate: true }
);

// emit
const emit = defineEmits<{
  change: [value: any];
}>();

/**
 * @description 选择筛选项
 * @param {Object} item 选中的哪项列表
 * @param {Object} option 选中的值
 * @return void
 * */
const select = (item: SelectDataProps, option: OptionsProps) => {
  if (!item.multiple) {
    // * 单选
    if (selected.value[item.key] !== option.value) selected.value[item.key] = option.value;
  } else {
    // * 多选
    // 如果选中的是第一个值,则直接设置
    if (item.options[0].value === option.value) selected.value[item.key] = [option.value];
    // 如果选择的值已经选中了,则删除选中的值
    if (selected.value[item.key].includes(option.value)) {
      let currentIndex = selected.value[item.key].findIndex((s: any) => s === option.value);
      selected.value[item.key].splice(currentIndex, 1);
      // 当全部删光时,把第第一个值选中
      if (selected.value[item.key].length == 0) selected.value[item.key] = [item.options[0].value];
    } else {
      // 未选中点击值的时候,追加选中值
      selected.value[item.key].push(option.value);
      // 单选中全部并且点击到了未选中的值,把第一个值删除掉
      if (selected.value[item.key].includes(item.options[0].value)) selected.value[item.key].splice(0, 1);
    }
  }
  emit("change", selected.value);
};
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/SelectIcon/index.scss
================================================
.icon-box {
  width: 100%;
  .el-button {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: var(--el-text-color-regular);
  }
  :deep(.el-dialog__body) {
    padding: 25px 20px 20px;
    .el-input {
      margin-bottom: 10px;
    }
    .icon-list {
      display: grid;
      grid-template-columns: repeat(auto-fill, 115px);
      justify-content: space-evenly;
      max-height: 70vh;
      .icon-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 42px;
        padding: 20px 30px;
        cursor: pointer;
        transition: all 0.2s;
        &:hover {
          transform: scale(1.3);
        }
        span {
          margin-top: 5px;
          line-height: 20px;
          text-align: center;
        }
      }
    }
  }
}


================================================
FILE: src/components/SelectIcon/index.vue
================================================
<template>
  <div class="icon-box">
    <el-input
      ref="inputRef"
      v-model="valueIcon"
      v-bind="$attrs"
      :placeholder="placeholder"
      :clearable="clearable"
      @clear="clearIcon"
      @click="openDialog"
    >
      <template #append>
        <el-button :icon="customIcons[iconValue]" />
      </template>
    </el-input>
    <el-dialog v-model="dialogVisible" :title="placeholder" top="50px" width="66%">
      <el-input v-model="inputValue" placeholder="搜索图标" size="large" :prefix-icon="Icons.Search" />
      <el-scrollbar v-if="Object.keys(iconsList).length">
        <div class="icon-list">
          <div v-for="item in iconsList" :key="item" class="icon-item" @click="selectIcon(item)">
            <component :is="item"></component>
            <span>{{ item.name }}</span>
          </div>
        </div>
      </el-scrollbar>
      <el-empty v-else description="未搜索到您要找的图标~" />
    </el-dialog>
  </div>
</template>

<script setup lang="ts" name="SelectIcon">
import { ref, computed } from "vue";
import * as Icons from "@element-plus/icons-vue";

interface SelectIconProps {
  iconValue: string;
  title?: string;
  clearable?: boolean;
  placeholder?: string;
}

const props = withDefaults(defineProps<SelectIconProps>(), {
  iconValue: "",
  title: "请选择图标",
  clearable: true,
  placeholder: "请选择图标"
});

// 重新接收一下,防止打包后 clearable 报错
const valueIcon = ref(props.iconValue);

// open Dialog
const dialogVisible = ref(false);
const openDialog = () => (dialogVisible.value = true);

// 选择图标(触发更新父组件数据)
const emit = defineEmits<{
  "update:iconValue": [value: string];
}>();
const selectIcon = (item: any) => {
  dialogVisible.value = false;
  valueIcon.value = item.name;
  emit("update:iconValue", item.name);
  setTimeout(() => inputRef.value.blur(), 0);
};

// 清空图标
const inputRef = ref();
const clearIcon = () => {
  valueIcon.value = "";
  emit("update:iconValue", "");
  setTimeout(() => inputRef.value.blur(), 0);
};

// 监听搜索框值
const inputValue = ref("");
const customIcons: { [key: string]: any } = Icons;
const iconsList = computed((): { [key: string]: any } => {
  if (!inputValue.value) return Icons;
  let result: { [key: string]: any } = {};
  for (const key in customIcons) {
    if (key.toLowerCase().indexOf(inputValue.value.toLowerCase()) > -1) result[key] = customIcons[key];
  }
  return result;
});
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/SvgIcon/index.vue
================================================
<template>
  <svg :style="iconStyle" aria-hidden="true">
    <use :xlink:href="symbolId" />
  </svg>
</template>

<script setup lang="ts" name="SvgIcon">
import { computed, CSSProperties } from "vue";

interface SvgProps {
  name: string; // 图标的名称 ==> 必传
  prefix?: string; // 图标的前缀 ==> 非必传(默认为"icon")
  iconStyle?: CSSProperties; // 图标的样式 ==> 非必传
}

const props = withDefaults(defineProps<SvgProps>(), {
  prefix: "icon",
  iconStyle: () => ({ width: "100px", height: "100px" })
});

const symbolId = computed(() => `#${props.prefix}-${props.name}`);
</script>


================================================
FILE: src/components/SwitchDark/index.vue
================================================
<template>
  <el-switch v-model="globalStore.isDark" inline-prompt :active-icon="Sunny" :inactive-icon="Moon" @change="switchDark" />
</template>

<script setup lang="ts" name="SwitchDark">
import { useTheme } from "@/hooks/useTheme";
import { useGlobalStore } from "@/stores/modules/global";
import { Sunny, Moon } from "@element-plus/icons-vue";

const { switchDark } = useTheme();
const globalStore = useGlobalStore();
</script>


================================================
FILE: src/components/TreeFilter/index.scss
================================================
.filter {
  box-sizing: border-box;
  width: 220px;
  height: 100%;
  padding: 18px;
  margin-right: 10px;
  .title {
    margin: 0 0 15px;
    font-size: 18px;
    font-weight: bold;
    color: var(--el-color-info-dark-2);
    letter-spacing: 0.5px;
  }
  .search {
    display: flex;
    align-items: center;
    margin: 0 0 15px;
    .el-icon {
      cursor: pointer;
      transform: rotate(90deg) translateY(-8px);
    }
  }
  .el-scrollbar {
    :deep(.el-tree) {
      height: 80%;
      overflow: auto;
      .el-tree-node__content {
        height: 33px;
      }
    }
    :deep(.el-tree--highlight-current) {
      .el-tree-node.is-current > .el-tree-node__content {
        background-color: var(--el-color-primary);
        .el-tree-node__label,
        .el-tree-node__expand-icon {
          color: white;
        }
        .is-leaf {
          color: transparent;
        }
      }
    }
  }
}


================================================
FILE: src/components/TreeFilter/index.vue
================================================
<template>
  <div class="card filter">
    <h4 v-if="title" class="title sle">
      {{ title }}
    </h4>
    <div class="search">
      <el-input v-model="filterText" placeholder="输入关键字进行过滤" clearable />
      <el-dropdown trigger="click">
        <el-icon size="20"><More /></el-icon>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item @click="toggleTreeNodes(true)">展开全部</el-dropdown-item>
            <el-dropdown-item @click="toggleTreeNodes(false)">折叠全部</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
    <el-scrollbar :style="{ height: title ? `calc(100% - 95px)` : `calc(100% - 56px)` }">
      <el-tree
        ref="treeRef"
        default-expand-all
        :node-key="id"
        :data="multiple ? treeData : treeAllData"
        :show-checkbox="multiple"
        :check-strictly="false"
        :current-node-key="!multiple ? selected : ''"
        :highlight-current="!multiple"
        :expand-on-click-node="false"
        :check-on-click-node="multiple"
        :props="defaultProps"
        :filter-node-method="filterNode"
        :default-checked-keys="multiple ? selected : []"
        @node-click="handleNodeClick"
        @check="handleCheckChange"
      >
        <template #default="scope">
          <span class="el-tree-node__label">
            <slot :row="scope">
              {{ scope.node.label }}
            </slot>
          </span>
        </template>
      </el-tree>
    </el-scrollbar>
  </div>
</template>

<script setup lang="ts" name="TreeFilter">
import { ref, watch, onBeforeMount, nextTick } from "vue";
import { ElTree } from "element-plus";

// 接收父组件参数并设置默认值
interface TreeFilterProps {
  requestApi?: (data?: any) => Promise<any>; // 请求分类数据的 api ==> 非必传
  data?: { [key: string]: any }[]; // 分类数据,如果有分类数据,则不会执行 api 请求 ==> 非必传
  title?: string; // treeFilter 标题 ==> 非必传
  id?: string; // 选择的id ==> 非必传,默认为 “id”
  label?: string; // 显示的label ==> 非必传,默认为 “label”
  multiple?: boolean; // 是否为多选 ==> 非必传,默认为 false
  defaultValue?: any; // 默认选中的值 ==> 非必传
}
const props = withDefaults(defineProps<TreeFilterProps>(), {
  id: "id",
  label: "label",
  multiple: false
});

const defaultProps = {
  children: "children",
  label: props.label
};

const treeRef = ref<InstanceType<typeof ElTree>>();
const treeData = ref<{ [key: string]: any }[]>([]);
const treeAllData = ref<{ [key: string]: any }[]>([]);

const selected = ref();
const setSelected = () => {
  if (props.multiple) selected.value = Array.isArray(props.defaultValue) ? props.defaultValue : [props.defaultValue];
  else selected.value = typeof props.defaultValue === "string" ? props.defaultValue : "";
};

onBeforeMount(async () => {
  setSelected();
  if (props.requestApi) {
    const { data } = await props.requestApi!();
    treeData.value = data;
    treeAllData.value = [{ id: "", [props.label]: "全部" }, ...data];
  }
});

// 使用 nextTick 防止打包后赋值不生效,开发环境是正常的
watch(
  () => props.defaultValue,
  () => nextTick(() => setSelected()),
  { deep: true, immediate: true }
);

watch(
  () => props.data,
  () => {
    if (props.data?.length) {
      treeData.value = props.data;
      treeAllData.value = [{ id: "", [props.label]: "全部" }, ...props.data];
    }
  },
  { deep: true, immediate: true }
);

const filterText = ref("");
watch(filterText, val => {
  treeRef.value!.filter(val);
});

// 过滤
const filterNode = (value: string, data: { [key: string]: any }, node: any) => {
  if (!value) return true;
  let parentNode = node.parent,
    labels = [node.label],
    level = 1;
  while (level < node.level) {
    labels = [...labels, parentNode.label];
    parentNode = parentNode.parent;
    level++;
  }
  return labels.some(label => label.indexOf(value) !== -1);
};

// 切换树节点的展开或折叠状态
const toggleTreeNodes = (isExpand: boolean) => {
  let nodes = treeRef.value?.store.nodesMap;
  if (!nodes) return;
  for (const node in nodes) {
    if (nodes.hasOwnProperty(node)) {
      nodes[node].expanded = isExpand;
    }
  }
};

// emit
const emit = defineEmits<{
  change: [value: any];
}>();

// 单选
const handleNodeClick = (data: { [key: string]: any }) => {
  if (props.multiple) return;
  emit("change", data[props.id]);
};

// 多选
const handleCheckChange = () => {
  emit("change", treeRef.value?.getCheckedKeys());
};

// 暴露给父组件使用
defineExpose({ treeData, treeAllData, treeRef });
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/components/Upload/Img.vue
================================================
<template>
  <div class="upload-box">
    <el-upload
      :id="uuid"
      action="#"
      :class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
      :multiple="false"
      :disabled="self_disabled"
      :show-file-list="false"
      :http-request="handleHttpUpload"
      :before-upload="beforeUpload"
      :on-success="uploadSuccess"
      :on-error="uploadError"
      :drag="drag"
      :accept="fileType.join(',')"
    >
      <template v-if="imageUrl">
        <img :src="imageUrl" class="upload-image" />
        <div class="upload-handle" @click.stop>
          <div v-if="!self_disabled" class="handle-icon" @click="editImg">
            <el-icon><Edit /></el-icon>
            <span>编辑</span>
          </div>
          <div class="handle-icon" @click="imgViewVisible = true">
            <el-icon><ZoomIn /></el-icon>
            <span>查看</span>
          </div>
          <div v-if="!self_disabled" class="handle-icon" @click="deleteImg">
            <el-icon><Delete /></el-icon>
            <span>删除</span>
          </div>
        </div>
      </template>
      <template v-else>
        <div class="upload-empty">
          <slot name="empty">
            <el-icon><Plus /></el-icon>
            <!-- <span>请上传图片</span> -->
          </slot>
        </div>
      </template>
    </el-upload>
    <div class="el-upload__tip">
      <slot name="tip"></slot>
    </div>
    <el-image-viewer v-if="imgViewVisible" :url-list="[imageUrl]" @close="imgViewVisible = false" />
  </div>
</template>

<script setup lang="ts" name="UploadImg">
import { ref, computed, inject } from "vue";
import { generateUUID } from "@/utils";
import { uploadImg } from "@/api/modules/upload";
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
import type { UploadProps, UploadRequestOptions } from "element-plus";

interface UploadFileProps {
  imageUrl: string; // 图片地址 ==> 必传
  api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
  drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
  disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
  fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
  height?: string; // 组件高度 ==> 非必传(默认为 150px)
  width?: string; // 组件宽度 ==> 非必传(默认为 150px)
  borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
}

// 接受父组件参数
const props = withDefaults(defineProps<UploadFileProps>(), {
  imageUrl: "",
  drag: true,
  disabled: false,
  fileSize: 5,
  fileType: () => ["image/jpeg", "image/png", "image/gif"],
  height: "150px",
  width: "150px",
  borderRadius: "8px"
});

// 生成组件唯一id
const uuid = ref("id-" + generateUUID());

// 查看图片
const imgViewVisible = ref(false);
// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0);
// 获取 el-form-item 组件上下文
const formItemContext = inject(formItemContextKey, void 0);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
  return props.disabled || formContext?.disabled;
});

/**
 * @description 图片上传
 * @param options upload 所有配置项
 * */
const emit = defineEmits<{
  "update:imageUrl": [value: string];
}>();
const handleHttpUpload = async (options: UploadRequestOptions) => {
  let formData = new FormData();
  formData.append("file", options.file);
  try {
    const api = props.api ?? uploadImg;
    const { data } = await api(formData);
    emit("update:imageUrl", data.fileUrl);
    // 调用 el-form 内部的校验方法(可自动校验)
    formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
  } catch (error) {
    options.onError(error as any);
  }
};

/**
 * @description 删除图片
 * */
const deleteImg = () => {
  emit("update:imageUrl", "");
};

/**
 * @description 编辑图片
 * */
const editImg = () => {
  const dom = document.querySelector(`#${uuid.value} .el-upload__input`);
  dom && dom.dispatchEvent(new MouseEvent("click"));
};

/**
 * @description 文件上传之前判断
 * @param rawFile 选择的文件
 * */
const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
  const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
  const imgType = props.fileType.includes(rawFile.type as File.ImageMimeType);
  if (!imgType)
    ElNotification({
      title: "温馨提示",
      message: "上传图片不符合所需的格式!",
      type: "warning"
    });
  if (!imgSize)
    setTimeout(() => {
      ElNotification({
        title: "温馨提示",
        message: `上传图片大小不能超过 ${props.fileSize}M!`,
        type: "warning"
      });
    }, 0);
  return imgType && imgSize;
};

/**
 * @description 图片上传成功
 * */
const uploadSuccess = () => {
  ElNotification({
    title: "温馨提示",
    message: "图片上传成功!",
    type: "success"
  });
};

/**
 * @description 图片上传错误
 * */
const uploadError = () => {
  ElNotification({
    title: "温馨提示",
    message: "图片上传失败,请您重新上传!",
    type: "error"
  });
};
</script>

<style scoped lang="scss">
.is-error {
  .upload {
    :deep(.el-upload),
    :deep(.el-upload-dragger) {
      border: 1px dashed var(--el-color-danger) !important;
      &:hover {
        border-color: var(--el-color-primary) !important;
      }
    }
  }
}
:deep(.disabled) {
  .el-upload,
  .el-upload-dragger {
    cursor: not-allowed !important;
    background: var(--el-disabled-bg-color);
    border: 1px dashed var(--el-border-color-darker) !important;
    &:hover {
      border: 1px dashed var(--el-border-color-darker) !important;
    }
  }
}
.upload-box {
  .no-border {
    :deep(.el-upload) {
      border: none !important;
    }
  }
  :deep(.upload) {
    .el-upload {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
      width: v-bind(width);
      height: v-bind(height);
      overflow: hidden;
      border: 1px dashed var(--el-border-color-darker);
      border-radius: v-bind(borderRadius);
      transition: var(--el-transition-duration-fast);
      &:hover {
        border-color: var(--el-color-primary);
        .upload-handle {
          opacity: 1;
        }
      }
      .el-upload-dragger {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 100%;
        padding: 0;
        overflow: hidden;
        background-color: transparent;
        border: 1px dashed var(--el-border-color-darker);
        border-radius: v-bind(borderRadius);
        &:hover {
          border: 1px dashed var(--el-color-primary);
        }
      }
      .el-upload-dragger.is-dragover {
        background-color: var(--el-color-primary-light-9);
        border: 2px dashed var(--el-color-primary) !important;
      }
      .upload-image {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
      .upload-empty {
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-size: 12px;
        line-height: 30px;
        color: var(--el-color-info);
        .el-icon {
          font-size: 28px;
          color: var(--el-text-color-secondary);
        }
      }
      .upload-handle {
        position: absolute;
        top: 0;
        right: 0;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 100%;
        cursor: pointer;
        background: rgb(0 0 0 / 60%);
        opacity: 0;
        transition: var(--el-transition-duration-fast);
        .handle-icon {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          padding: 0 6%;
          color: aliceblue;
          .el-icon {
            margin-bottom: 40%;
            font-size: 130%;
            line-height: 130%;
          }
          span {
            font-size: 85%;
            line-height: 85%;
          }
        }
      }
    }
  }
  .el-upload__tip {
    line-height: 18px;
    text-align: center;
  }
}
</style>


================================================
FILE: src/components/Upload/Imgs.vue
================================================
<template>
  <div class="upload-box">
    <el-upload
      v-model:file-list="_fileList"
      action="#"
      list-type="picture-card"
      :class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
      :multiple="true"
      :disabled="self_disabled"
      :limit="limit"
      :http-request="handleHttpUpload"
      :before-upload="beforeUpload"
      :on-exceed="handleExceed"
      :on-success="uploadSuccess"
      :on-error="uploadError"
      :drag="drag"
      :accept="fileType.join(',')"
    >
      <div class="upload-empty">
        <slot name="empty">
          <el-icon><Plus /></el-icon>
          <!-- <span>请上传图片</span> -->
        </slot>
      </div>
      <template #file="{ file }">
        <img :src="file.url" class="upload-image" />
        <div class="upload-handle" @click.stop>
          <div class="handle-icon" @click="handlePictureCardPreview(file)">
            <el-icon><ZoomIn /></el-icon>
            <span>查看</span>
          </div>
          <div v-if="!self_disabled" class="handle-icon" @click="handleRemove(file)">
            <el-icon><Delete /></el-icon>
            <span>删除</span>
          </div>
        </div>
      </template>
    </el-upload>
    <div class="el-upload__tip">
      <slot name="tip"></slot>
    </div>
    <el-image-viewer v-if="imgViewVisible" :url-list="[viewImageUrl]" @close="imgViewVisible = false" />
  </div>
</template>

<script setup lang="ts" name="UploadImgs">
import { ref, computed, inject, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { uploadImg } from "@/api/modules/upload";
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";

interface UploadFileProps {
  fileList: UploadUserFile[];
  api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
  drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
  disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
  limit?: number; // 最大图片上传数 ==> 非必传(默认为 5张)
  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
  fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
  height?: string; // 组件高度 ==> 非必传(默认为 150px)
  width?: string; // 组件宽度 ==> 非必传(默认为 150px)
  borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
}

const props = withDefaults(defineProps<UploadFileProps>(), {
  fileList: () => [],
  drag: true,
  disabled: false,
  limit: 5,
  fileSize: 5,
  fileType: () => ["image/jpeg", "image/png", "image/gif"],
  height: "150px",
  width: "150px",
  borderRadius: "8px"
});

// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0);
// 获取 el-form-item 组件上下文
const formItemContext = inject(formItemContextKey, void 0);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
  return props.disabled || formContext?.disabled;
});

const _fileList = ref<UploadUserFile[]>(props.fileList);

// 监听 props.fileList 列表默认值改变
watch(
  () => props.fileList,
  (n: UploadUserFile[]) => {
    _fileList.value = n;
  }
);

/**
 * @description 文件上传之前判断
 * @param rawFile 选择的文件
 * */
const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
  const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
  const imgType = props.fileType.includes(rawFile.type as File.ImageMimeType);
  if (!imgType)
    ElNotification({
      title: "温馨提示",
      message: "上传图片不符合所需的格式!",
      type: "warning"
    });
  if (!imgSize)
    setTimeout(() => {
      ElNotification({
        title: "温馨提示",
        message: `上传图片大小不能超过 ${props.fileSize}M!`,
        type: "warning"
      });
    }, 0);
  return imgType && imgSize;
};

/**
 * @description 图片上传
 * @param options upload 所有配置项
 * */
const handleHttpUpload = async (options: UploadRequestOptions) => {
  let formData = new FormData();
  formData.append("file", options.file);
  try {
    const api = props.api ?? uploadImg;
    const { data } = await api(formData);
    options.onSuccess(data);
  } catch (error) {
    options.onError(error as any);
  }
};

/**
 * @description 图片上传成功
 * @param response 上传响应结果
 * @param uploadFile 上传的文件
 * */
const emit = defineEmits<{
  "update:fileList": [value: UploadUserFile[]];
}>();
const uploadSuccess = (response: { fileUrl: string } | undefined, uploadFile: UploadFile) => {
  if (!response) return;
  uploadFile.url = response.fileUrl;
  emit("update:fileList", _fileList.value);
  // 调用 el-form 内部的校验方法(可自动校验)
  formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
  ElNotification({
    title: "温馨提示",
    message: "图片上传成功!",
    type: "success"
  });
};

/**
 * @description 删除图片
 * @param file 删除的文件
 * */
const handleRemove = (file: UploadFile) => {
  _fileList.value = _fileList.value.filter(item => item.url !== file.url || item.name !== file.name);
  emit("update:fileList", _fileList.value);
};

/**
 * @description 图片上传错误
 * */
const uploadError = () => {
  ElNotification({
    title: "温馨提示",
    message: "图片上传失败,请您重新上传!",
    type: "error"
  });
};

/**
 * @description 文件数超出
 * */
const handleExceed = () => {
  ElNotification({
    title: "温馨提示",
    message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`,
    type: "warning"
  });
};

/**
 * @description 图片预览
 * @param file 预览的文件
 * */
const viewImageUrl = ref("");
const imgViewVisible = ref(false);
const handlePictureCardPreview: UploadProps["onPreview"] = file => {
  viewImageUrl.value = file.url!;
  imgViewVisible.value = true;
};
</script>

<style scoped lang="scss">
.is-error {
  .upload {
    :deep(.el-upload--picture-card),
    :deep(.el-upload-dragger) {
      border: 1px dashed var(--el-color-danger) !important;
      &:hover {
        border-color: var(--el-color-primary) !important;
      }
    }
  }
}
:deep(.disabled) {
  .el-upload--picture-card,
  .el-upload-dragger {
    cursor: not-allowed;
    background: var(--el-disabled-bg-color) !important;
    border: 1px dashed var(--el-border-color-darker);
    &:hover {
      border-color: var(--el-border-color-darker) !important;
    }
  }
}
.upload-box {
  .no-border {
    :deep(.el-upload--picture-card) {
      border: none !important;
    }
  }
  :deep(.upload) {
    .el-upload-dragger {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 100%;
      height: 100%;
      padding: 0;
      overflow: hidden;
      border: 1px dashed var(--el-border-color-darker);
      border-radius: v-bind(borderRadius);
      &:hover {
        border: 1px dashed var(--el-color-primary);
      }
    }
    .el-upload-dragger.is-dragover {
      background-color: var(--el-color-primary-light-9);
      border: 2px dashed var(--el-color-primary) !important;
    }
    .el-upload-list__item,
    .el-upload--picture-card {
      width: v-bind(width);
      height: v-bind(height);
      background-color: transparent;
      border-radius: v-bind(borderRadius);
    }
    .upload-image {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .upload-handle {
      position: absolute;
      top: 0;
      right: 0;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 100%;
      height: 100%;
      cursor: pointer;
      background: rgb(0 0 0 / 60%);
      opacity: 0;
      transition: var(--el-transition-duration-fast);
      .handle-icon {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 0 6%;
        color: aliceblue;
        .el-icon {
          margin-bottom: 15%;
          font-size: 140%;
        }
        span {
          font-size: 100%;
        }
      }
    }
    .el-upload-list__item {
      &:hover {
        .upload-handle {
          opacity: 1;
        }
      }
    }
    .upload-empty {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-size: 12px;
      line-height: 30px;
      color: var(--el-color-info);
      .el-icon {
        font-size: 28px;
        color: var(--el-text-color-secondary);
      }
    }
  }
  .el-upload__tip {
    line-height: 15px;
    text-align: center;
  }
}
</style>


================================================
FILE: src/components/WangEditor/index.scss
================================================
/* 富文本组件校验失败样式 */
.is-error {
  .editor-box {
    border-color: var(--el-color-danger);
    .editor-toolbar {
      border-bottom-color: var(--el-color-danger);
    }
  }
}

/* 富文本组件禁用样式 */
.editor-disabled {
  cursor: not-allowed !important;
}

/* 富文本组件样式 */
.editor-box {
  /* 防止富文本编辑器全屏时 tabs组件 在其层级之上 */
  z-index: 2;
  width: 100%;
  border: 1px solid var(--el-border-color-darker);
  .editor-toolbar {
    border-bottom: 1px solid var(--el-border-color-darker);
  }
  .editor-content {
    overflow-y: hidden;
  }
}


================================================
FILE: src/components/WangEditor/index.vue
================================================
<template>
  <div :class="['editor-box', self_disabled ? 'editor-disabled' : '']">
    <Toolbar v-if="!hideToolBar" class="editor-toolbar" :editor="editorRef" :default-config="toolbarConfig" :mode="mode" />
    <Editor
      v-model="valueHtml"
      class="editor-content"
      :style="{ height }"
      :mode="mode"
      :default-config="editorConfig"
      @on-created="handleCreated"
      @on-blur="handleBlur"
    />
  </div>
</template>

<script setup lang="ts" name="WangEditor">
import { nextTick, computed, inject, shallowRef, onBeforeUnmount } from "vue";
import { IToolbarConfig, IEditorConfig } from "@wangeditor/editor";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { uploadImg, uploadVideo } from "@/api/modules/upload";
import "@wangeditor/editor/dist/css/style.css";
import { formContextKey, formItemContextKey } from "element-plus";

// 富文本 DOM 元素
const editorRef = shallowRef();

// 实列化编辑器
const handleCreated = (editor: any) => {
  editorRef.value = editor;
};

// 接收父组件参数,并设置默认值
interface RichEditorProps {
  value: string; // 富文本值 ==> 必传
  toolbarConfig?: Partial<IToolbarConfig>; // 工具栏配置 ==> 非必传(默认为空)
  editorConfig?: Partial<IEditorConfig>; // 编辑器配置 ==> 非必传(默认为空)
  height?: string; // 富文本高度 ==> 非必传(默认为 500px)
  mode?: "default" | "simple"; // 富文本模式 ==> 非必传(默认为 default)
  hideToolBar?: boolean; // 是否隐藏工具栏 ==> 非必传(默认为false)
  disabled?: boolean; // 是否禁用编辑器 ==> 非必传(默认为false)
}
const props = withDefaults(defineProps<RichEditorProps>(), {
  toolbarConfig: () => {
    return {
      excludeKeys: []
    };
  },
  editorConfig: () => {
    return {
      placeholder: "请输入内容...",
      MENU_CONF: {}
    };
  },
  height: "500px",
  mode: "default",
  hideToolBar: false,
  disabled: false
});

// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0);
// 获取 el-form-item 组件上下文
const formItemContext = inject(formItemContextKey, void 0);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
  return props.disabled || formContext?.disabled;
});

// 判断当前富文本编辑器是否禁用
if (self_disabled.value) nextTick(() => editorRef.value.disable());

// 富文本的内容监听,触发父组件改变,实现双向数据绑定
const emit = defineEmits<{
  "update:value": [value: string];
  "check-validate": [];
}>();
const valueHtml = computed({
  get() {
    return props.value;
  },
  set(val: string) {
    // 防止富文本内容为空时,校验失败
    if (editorRef.value.isEmpty()) val = "";
    emit("update:value", val);
  }
});

/**
 * @description 图片自定义上传
 * @param file 上传的文件
 * @param insertFn 上传成功后的回调函数(插入到富文本编辑器中)
 * */
type InsertFnTypeImg = (url: string, alt?: string, href?: string) => void;
props.editorConfig.MENU_CONF!["uploadImage"] = {
  async customUpload(file: File, insertFn: InsertFnTypeImg) {
    if (!uploadImgValidate(file)) return;
    let formData = new FormData();
    formData.append("file", file);
    try {
      const { data } = await uploadImg(formData);
      insertFn(data.fileUrl);
    } catch (error) {
      console.log(error);
    }
  }
};

// 图片上传前判断
const uploadImgValidate = (file: File): boolean => {
  console.log(file);
  return true;
};

/**
 * @description 视频自定义上传
 * @param file 上传的文件
 * @param insertFn 上传成功后的回调函数(插入到富文本编辑器中)
 * */
type InsertFnTypeVideo = (url: string, poster?: string) => void;
props.editorConfig.MENU_CONF!["uploadVideo"] = {
  async customUpload(file: File, insertFn: InsertFnTypeVideo) {
    if (!uploadVideoValidate(file)) return;
    let formData = new FormData();
    formData.append("file", file);
    try {
      const { data } = await uploadVideo(formData);
      insertFn(data.fileUrl);
    } catch (error) {
      console.log(error);
    }
  }
};

// 视频上传前判断
const uploadVideoValidate = (file: File): boolean => {
  console.log(file);
  return true;
};

// 编辑框失去焦点时触发
const handleBlur = () => {
  formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
};

// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
  if (!editorRef.value) return;
  editorRef.value.destroy();
});

defineExpose({
  editor: editorRef
});
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>


================================================
FILE: src/config/index.ts
================================================
// ? 全局默认配置项

// 首页地址(默认)
export const HOME_URL: string = "/home/index";

// 登录页地址(默认)
export const LOGIN_URL: string = "/login";

// 默认主题颜色
export const DEFAULT_PRIMARY: string = "#009688";

// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
export const ROUTER_WHITE_LIST: string[] = ["/500"];

// 高德地图 key
export const AMAP_MAP_KEY: string = "";

// 百度地图 key
export const BAIDU_MAP_KEY: string = "";


================================================
FILE: src/config/nprogress.ts
================================================
import NProgress from "nprogress";
import "nprogress/nprogress.css";

NProgress.configure({
  easing: "ease", // 动画方式
  speed: 500, // 递增进度条的速度
  showSpinner: true, // 是否显示加载ico
  trickleSpeed: 200, // 自动递增间隔
  minimum: 0.3 // 初始化时的最小百分比
});

export default NProgress;


================================================
FILE: src/directives/index.ts
================================================
import { App, Directive } from "vue";
import auth from "./modules/auth";
import copy from "./modules/copy";
import waterMarker from "./modules/waterMarker";
import draggable from "./modules/draggable";
import debounce from "./modules/debounce";
import throttle from "./modules/throttle";
import longpress from "./modules/longpress";

const directivesList: { [key: string]: Directive } = {
  auth,
  copy,
  waterMarker,
  draggable,
  debounce,
  throttle,
  longpress
};

const directives = {
  install: function (app: App<Element>) {
    Object.keys(directivesList).forEach(key => {
      app.directive(key, directivesList[key]);
    });
  }
};

export default directives;


================================================
FILE: src/directives/modules/auth.ts
================================================
/**
 * v-auth
 * 按钮权限指令
 */
import { useAuthStore } from "@/stores/modules/auth";
import type { Directive, DirectiveBinding } from "vue";

const auth: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const { value } = binding;
    const authStore = useAuthStore();
    const currentPageRoles = authStore.authButtonListGet[authStore.routeName] ?? [];
    if (value instanceof Array && value.length) {
      const hasPermission = value.every(item => currentPageRoles.includes(item));
      if (!hasPermission) el.remove();
    } else {
      if (!currentPageRoles.includes(value)) el.remove();
    }
  }
};

export default auth;


================================================
FILE: src/directives/modules/copy.ts
================================================
/**
 * v-copy
 * 复制某个值至剪贴板
 * 接收参数:string类型/Ref<string>类型/Reactive<string>类型
 */

import type { Directive, DirectiveBinding } from "vue";
import { ElMessage } from "element-plus";
interface ElType extends HTMLElement {
  copyData: string | number;
}
const copy: Directive = {
  mounted(el: ElType, binding: DirectiveBinding) {
    el.copyData = binding.value;
    el.addEventListener("click", handleClick);
  },
  updated(el: ElType, binding: DirectiveBinding) {
    el.copyData = binding.value;
  },
  beforeUnmount(el: ElType) {
    el.removeEventListener("click", handleClick);
  }
};

async function handleClick(this: any) {
  try {
    await navigator.clipboard.writeText(this.copyData);
    ElMessage({
      type: "success",
      message: "复制成功"
    });
  } catch (err) {
    console.error("复制操作不被支持或失败: ", err);
  }
}

export default copy;


================================================
FILE: src/directives/modules/debounce.ts
================================================
/**
 * v-debounce
 * 按钮防抖指令,可自行扩展至input
 * 接收参数:function类型
 */
import type { Directive, DirectiveBinding } from "vue";
interface ElType extends HTMLElement {
  __handleClick__: () => any;
}
const debounce: Directive = {
  mounted(el: ElType, binding: DirectiveBinding) {
    if (typeof binding.value !== "function") {
      throw "callback must be a function";
    }
    let timer: NodeJS.Timeout | null = null;
    el.__handleClick__ = function () {
      if (timer) {
        clearInterval(timer);
      }
      timer = setTimeout(() => {
        binding.value();
      }, 500);
    };
    el.addEventListener("click", el.__handleClick__);
  },
  beforeUnmount(el: ElType) {
    el.removeEventListener("click", el.__handleClick__);
  }
};

export default debounce;


================================================
FILE: src/directives/modules/draggable.ts
================================================
/*
	需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。

	思路:
		1、设置需要拖拽的元素为absolute,其父元素为relative。
		2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
		3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
		4、鼠标松开(onmouseup)时完成一次拖拽

	使用:在 Dom 上加上 v-draggable 即可
	<div class="dialog-model" v-draggable></div>
*/
import type { Directive } from "vue";
interface ElType extends HTMLElement {
  parentNode: any;
}
const draggable: Directive = {
  mounted: function (el: ElType) {
    el.style.cursor = "move";
    el.style.position = "absolute";
    el.onmousedown = function (e) {
      let disX = e.pageX - el.offsetLeft;
      let disY = e.pageY - el.offsetTop;
      document.onmousemove = function (e) {
        let x = e.pageX - disX;
        let y = e.pageY - disY;
        let maxX = el.parentNode.offsetWidth - el.offsetWidth;
        let maxY = el.parentNode.offsetHeight - el.offsetHeight;
        if (x < 0) {
          x = 0;
        } else if (x > maxX) {
          x = maxX;
        }

        if (y < 0) {
          y = 0;
        } else if (y > maxY) {
          y = maxY;
        }
        el.style.left = x + "px";
        el.style.top = y + "px";
      };
      document.onmouseup = function () {
        document.onmousemove = document.onmouseup = null;
      };
    };
  }
};
export default draggable;


================================================
FILE: src/directives/modules/longpress.ts
================================================
/**
 * v-longpress
 * 长按指令,长按时触发事件
 */
import type { Directive, DirectiveBinding } from "vue";

const directive: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    if (typeof binding.value !== "function") {
      throw "callback must be a function";
    }
    // 定义变量
    let pressTimer: any = null;
    // 创建计时器( 2秒后执行函数 )
    const start = (e: any) => {
      if (e.button) {
        if (e.type === "click" && e.button !== 0) {
          return;
        }
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler(e);
        }, 1000);
      }
    };
    // 取消计时器
    const cancel = () => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer);
        pressTimer = null;
      }
    };
    // 运行函数
    const handler = (e: MouseEvent | TouchEvent) => {
      binding.value(e);
    };
    // 添加事件监听器
    el.addEventListener("mousedown", start);
    el.addEventListener("touchstart", start);
    // 取消计时器
    el.addEventListener("click", cancel);
    el.addEventListener("mouseout", cancel);
    el.addEventListener("touchend", cancel);
    el.addEventListener("touchcancel", cancel);
  }
};

export default directive;


================================================
FILE: src/directives/modules/throttle.ts
================================================
/*
  需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。

  思路:
    1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮
    2、将需要触发的方法绑定在指令上
  
  使用:给 Dom 加上 v-throttle 及回调函数即可
  <button v-throttle="debounceClick">节流提交</button>
*/
import type { Directive, DirectiveBinding } from "vue";
interface ElType extends HTMLElement {
  __handleClick__: () => any;
  disabled: boolean;
}
const throttle: Directive = {
  mounted(el: ElType, binding: DirectiveBinding) {
    if (typeof binding.value !== "function") {
      throw "callback must be a function";
    }
    let timer: NodeJS.Timeout | null = null;
    el.__handleClick__ = function () {
      if (timer) {
        clearTimeout(timer);
      }
      if (!el.disabled) {
        el.disabled = true;
        binding.value();
        timer = setTimeout(() => {
          el.disabled = false;
        }, 1000);
      }
    };
    el.addEventListener("click", el.__handleClick__);
  },
  beforeUnmount(el: ElType) {
    el.removeEventListener("click", el.__handleClick__);
  }
};

export default throttle;


================================================
FILE: src/directives/modules/waterMarker.ts
================================================
/*
  需求:给整个页面添加背景水印。

  思路:
    1、使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。
    2、将其设置为背景图片,从而实现页面或组件水印效果
  
  使用:设置水印文案,颜色,字体大小即可
  <div v-waterMarker="{text:'版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
*/

import type { Directive, DirectiveBinding } from "vue";
const addWaterMarker: Directive = (str: string, parentNode: any, font: any, textColor: string) => {
  // 水印文字,父元素,字体,文字颜色
  let can: HTMLCanvasElement = document.createElement("canvas");
  parentNode.appendChild(can);
  can.width = 205;
  can.height = 140;
  can.style.display = "none";
  let cans = can.getContext("2d") as CanvasRenderingContext2D;
  cans.rotate((-20 * Math.PI) / 180);
  cans.font = font || "16px Microsoft JhengHei";
  cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
  cans.textAlign = "left";
  cans.textBaseline = "Middle" as CanvasTextBaseline;
  cans.fillText(str, can.width / 10, can.height / 2);
  parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
};

const waterMarker = {
  mounted(el: DirectiveBinding, binding: DirectiveBinding) {
    addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor);
  }
};

export default waterMarker;


================================================
FILE: src/enums/httpEnum.ts
================================================
/**
 * @description:请求配置
 */
export enum ResultEnum {
  SUCCESS = 200,
  ERROR = 500,
  OVERDUE = 401,
  TIMEOUT = 30000,
  TYPE = "success"
}

/**
 * @description:请求方法
 */
export enum RequestEnum {
  GET = "GET",
  POST = "POST",
  PATCH = "PATCH",
  PUT = "PUT",
  DELETE = "DELETE"
}

/**
 * @description:常用的 contentTyp 类型
 */
export enum ContentTypeEnum {
  // json
  JSON = "application/json;charset=UTF-8",
  // text
  TEXT = "text/plain;charset=UTF-8",
  // form-data 一般配合qs
  FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",
  // form-data 上传
  FORM_DATA = "multipart/form-data;charset=UTF-8"
}


================================================
FILE: src/hooks/interface/index.ts
================================================
export namespace Table {
  export interface Pageable {
    pageNum: number;
    pageSize: number;
    total: number;
  }
  export interface StateProps {
    tableData: any[];
    pageable: Pageable;
    searchParam: {
      [key: string]: any;
    };
    searchInitParam: {
      [key: string]: any;
    };
    totalParam: {
      [key: string]: any;
    };
    icon?: {
      [key: string]: any;
    };
  }
}

export namespace HandleData {
  export type MessageType = "" | "success" | "warning" | "info" | "error";
}

export namespace Theme {
  export type ThemeType = "light" | "inverted" | "dark";
  export type GreyOrWeakType = "grey" | "weak";
}


================================================
FILE: src/hooks/useAuthButtons.ts
================================================
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/modules/auth";

/**
 * @description 页面按钮权限
 * */
export const useAuthButtons = () => {
  const route = useRoute();
  const authStore = useAuthStore();
  const authButtons = authStore.authButtonListGet[route.name as string] || [];

  const BUTTONS = computed(() => {
    let currentPageAuthButton: { [key: string]: boolean } = {};
    authButtons.forEach(item => (currentPageAuthButton[item] = true));
    return currentPageAuthButton;
  });

  return {
    BUTTONS
  };
};


================================================
FILE: src/hooks/useDownload.ts
================================================
import { ElNotification } from "element-plus";

/**
 * @description 接收数据流生成 blob,创建链接,下载文件
 * @param {Function} api 导出表格的api方法 (必传)
 * @param {String} tempName 导出的文件名 (必传)
 * @param {Object} params 导出的参数 (默认{})
 * @param {Boolean} isNotify 是否有导出消息提示 (默认为 true)
 * @param {String} fileType 导出的文件格式 (默认为.xlsx)
 * */
export const useDownload = async (
  api: (param: any) => Promise<any>,
  tempName: string,
  params: any = {},
  isNotify: boolean 
Download .txt
gitextract_l6iyifel/

├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .husky/
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.cjs
├── .stylelintignore
├── .stylelintrc.cjs
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build/
│   ├── getEnv.ts
│   ├── plugins.ts
│   └── proxy.ts
├── commitlint.config.cjs
├── index.html
├── lint-staged.config.cjs
├── package.json
├── postcss.config.cjs
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── config/
│   │   │   └── servicePort.ts
│   │   ├── helper/
│   │   │   ├── axiosCancel.ts
│   │   │   └── checkStatus.ts
│   │   ├── index.ts
│   │   ├── interface/
│   │   │   └── index.ts
│   │   └── modules/
│   │       ├── login.ts
│   │       ├── upload.ts
│   │       └── user.ts
│   ├── assets/
│   │   ├── fonts/
│   │   │   ├── DIN.otf
│   │   │   └── font.scss
│   │   ├── iconfont/
│   │   │   └── iconfont.scss
│   │   └── json/
│   │       ├── authButtonList.json
│   │       └── authMenuList.json
│   ├── components/
│   │   ├── ECharts/
│   │   │   ├── config/
│   │   │   │   └── index.ts
│   │   │   └── index.vue
│   │   ├── ErrorMessage/
│   │   │   ├── 403.vue
│   │   │   ├── 404.vue
│   │   │   ├── 500.vue
│   │   │   └── index.scss
│   │   ├── Grid/
│   │   │   ├── components/
│   │   │   │   └── GridItem.vue
│   │   │   ├── index.vue
│   │   │   └── interface/
│   │   │       └── index.ts
│   │   ├── ImportExcel/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── Loading/
│   │   │   ├── fullScreen.ts
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── ProTable/
│   │   │   ├── components/
│   │   │   │   ├── ColSetting.vue
│   │   │   │   ├── Pagination.vue
│   │   │   │   └── TableColumn.vue
│   │   │   ├── index.vue
│   │   │   └── interface/
│   │   │       └── index.ts
│   │   ├── SearchForm/
│   │   │   ├── components/
│   │   │   │   └── SearchFormItem.vue
│   │   │   └── index.vue
│   │   ├── SelectFilter/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── SelectIcon/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── SvgIcon/
│   │   │   └── index.vue
│   │   ├── SwitchDark/
│   │   │   └── index.vue
│   │   ├── TreeFilter/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── Upload/
│   │   │   ├── Img.vue
│   │   │   └── Imgs.vue
│   │   └── WangEditor/
│   │       ├── index.scss
│   │       └── index.vue
│   ├── config/
│   │   ├── index.ts
│   │   └── nprogress.ts
│   ├── directives/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── auth.ts
│   │       ├── copy.ts
│   │       ├── debounce.ts
│   │       ├── draggable.ts
│   │       ├── longpress.ts
│   │       ├── throttle.ts
│   │       └── waterMarker.ts
│   ├── enums/
│   │   └── httpEnum.ts
│   ├── hooks/
│   │   ├── interface/
│   │   │   └── index.ts
│   │   ├── useAuthButtons.ts
│   │   ├── useDownload.ts
│   │   ├── useHandleData.ts
│   │   ├── useOnline.ts
│   │   ├── useSelection.ts
│   │   ├── useTable.ts
│   │   ├── useTheme.ts
│   │   └── useTime.ts
│   ├── languages/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── en.ts
│   │       └── zh.ts
│   ├── layouts/
│   │   ├── LayoutClassic/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutColumns/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutTransverse/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── LayoutVertical/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── components/
│   │   │   ├── Footer/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── Header/
│   │   │   │   ├── ToolBarLeft.vue
│   │   │   │   ├── ToolBarRight.vue
│   │   │   │   └── components/
│   │   │   │       ├── AssemblySize.vue
│   │   │   │       ├── Avatar.vue
│   │   │   │       ├── Breadcrumb.vue
│   │   │   │       ├── CollapseIcon.vue
│   │   │   │       ├── Fullscreen.vue
│   │   │   │       ├── InfoDialog.vue
│   │   │   │       ├── Language.vue
│   │   │   │       ├── Message.vue
│   │   │   │       ├── PasswordDialog.vue
│   │   │   │       ├── SearchMenu.vue
│   │   │   │       └── ThemeSetting.vue
│   │   │   ├── Main/
│   │   │   │   ├── components/
│   │   │   │   │   └── Maximize.vue
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── Menu/
│   │   │   │   └── SubMenu.vue
│   │   │   ├── Tabs/
│   │   │   │   ├── components/
│   │   │   │   │   └── MoreButton.vue
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── ThemeDrawer/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── index.vue
│   │   └── indexAsync.vue
│   ├── main.ts
│   ├── routers/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── dynamicRouter.ts
│   │       └── staticRouter.ts
│   ├── stores/
│   │   ├── helper/
│   │   │   └── persist.ts
│   │   ├── index.ts
│   │   ├── interface/
│   │   │   └── index.ts
│   │   └── modules/
│   │       ├── auth.ts
│   │       ├── global.ts
│   │       ├── keepAlive.ts
│   │       ├── tabs.ts
│   │       └── user.ts
│   ├── styles/
│   │   ├── common.scss
│   │   ├── element-dark.scss
│   │   ├── element.scss
│   │   ├── reset.scss
│   │   ├── theme/
│   │   │   ├── aside.ts
│   │   │   ├── header.ts
│   │   │   └── menu.ts
│   │   └── var.scss
│   ├── typings/
│   │   ├── global.d.ts
│   │   ├── utils.d.ts
│   │   └── window.d.ts
│   ├── utils/
│   │   ├── color.ts
│   │   ├── dict.ts
│   │   ├── eleValidate.ts
│   │   ├── errorHandler.ts
│   │   ├── index.ts
│   │   ├── is/
│   │   │   └── index.ts
│   │   ├── mittBus.ts
│   │   └── svg.ts
│   ├── views/
│   │   ├── about/
│   │   │   └── index.vue
│   │   ├── assembly/
│   │   │   ├── batchImport/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── draggable/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── guide/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── selectFilter/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── selectIcon/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── svgIcon/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── tabs/
│   │   │   │   ├── detail.vue
│   │   │   │   └── index.vue
│   │   │   ├── treeFilter/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── uploadFile/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── wangEditor/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── auth/
│   │   │   ├── button/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── menu/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── dashboard/
│   │   │   └── dataVisualize/
│   │   │       ├── components/
│   │   │       │   ├── curve.vue
│   │   │       │   └── pie.vue
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── dataScreen/
│   │   │   ├── assets/
│   │   │   │   ├── alarmList.Json
│   │   │   │   ├── china.json
│   │   │   │   └── ranking-icon.ts
│   │   │   ├── components/
│   │   │   │   ├── AgeRatioChart.vue
│   │   │   │   ├── AnnualUseChart.vue
│   │   │   │   ├── ChinaMapChart.vue
│   │   │   │   ├── HotPlateChart.vue
│   │   │   │   ├── MaleFemaleRatioChart.vue
│   │   │   │   ├── OverNext30Chart.vue
│   │   │   │   ├── PlatformSourceChart.vue
│   │   │   │   └── RealTimeAccessChart.vue
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── directives/
│   │   │   ├── copyDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── debounceDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── dragDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── longpressDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── throttleDirect/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── watermarkDirect/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── echarts/
│   │   │   ├── columnChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── lineChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── nestedChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── pieChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── radarChart/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── waterChart/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── form/
│   │   │   ├── basicForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── dynamicForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── proForm/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── validateForm/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── home/
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── link/
│   │   │   ├── bing/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── docs/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── gitee/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── github/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   └── juejin/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── login/
│   │   │   ├── components/
│   │   │   │   └── LoginForm.vue
│   │   │   ├── index.scss
│   │   │   └── index.vue
│   │   ├── menu/
│   │   │   ├── menu1/
│   │   │   │   ├── index.scss
│   │   │   │   └── index.vue
│   │   │   ├── menu2/
│   │   │   │   ├── menu21/
│   │   │   │   │   ├── index.scss
│   │   │   │   │   └── index.vue
│   │   │   │   ├── menu22/
│   │   │   │   │   ├── menu221/
│   │   │   │   │   │   ├── index.scss
│   │   │   │   │   │   └── index.vue
│   │   │   │   │   └── menu222/
│   │   │   │   │       ├── index.scss
│   │   │   │   │       └── index.vue
│   │   │   │   └── menu23/
│   │   │   │       ├── index.scss
│   │   │   │       └── index.vue
│   │   │   └── menu3/
│   │   │       ├── index.scss
│   │   │       └── index.vue
│   │   ├── proTable/
│   │   │   ├── complexProTable/
│   │   │   │   └── index.vue
│   │   │   ├── components/
│   │   │   │   └── UserDrawer.vue
│   │   │   ├── document/
│   │   │   │   └── index.vue
│   │   │   ├── treeProTable/
│   │   │   │   └── index.vue
│   │   │   ├── useProTable/
│   │   │   │   ├── detail.vue
│   │   │   │   └── index.vue
│   │   │   ├── useSelectFilter/
│   │   │   │   └── index.vue
│   │   │   └── useTreeFilter/
│   │   │       ├── detail.vue
│   │   │       └── index.vue
│   │   └── system/
│   │       ├── accountManage/
│   │       │   └── index.vue
│   │       ├── departmentManage/
│   │       │   └── index.vue
│   │       ├── dictManage/
│   │       │   └── index.vue
│   │       ├── menuMange/
│   │       │   └── index.vue
│   │       ├── roleManage/
│   │       │   └── index.vue
│   │       ├── systemLog/
│   │       │   └── index.vue
│   │       └── timingTask/
│   │           └── index.vue
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (155 symbols across 32 files)

FILE: build/getEnv.ts
  function isDevFn (line 3) | function isDevFn(mode: string): boolean {
  function isProdFn (line 7) | function isProdFn(mode: string): boolean {
  function isTestFn (line 11) | function isTestFn(mode: string): boolean {
  function isReportMode (line 18) | function isReportMode(): boolean {
  function wrapperEnv (line 23) | function wrapperEnv(envConf: Recordable): ViteEnv {
  function getRootPath (line 44) | function getRootPath(...dir: string[]) {

FILE: build/proxy.ts
  type ProxyItem (line 3) | type ProxyItem = [string, string];
  type ProxyList (line 5) | type ProxyList = ProxyItem[];
  type ProxyTargetList (line 7) | type ProxyTargetList = Record<string, ProxyOptions>;
  function createProxy (line 13) | function createProxy(list: ProxyList = []) {

FILE: src/api/config/servicePort.ts
  constant PORT1 (line 2) | const PORT1 = "/geeker";
  constant PORT2 (line 3) | const PORT2 = "/hooks";

FILE: src/api/helper/axiosCancel.ts
  class AxiosCanceler (line 17) | class AxiosCanceler {
    method addPending (line 23) | addPending(config: CustomAxiosRequestConfig) {
    method removePending (line 36) | removePending(config: CustomAxiosRequestConfig) {
    method removeAllPending (line 49) | removeAllPending() {

FILE: src/api/index.ts
  type CustomAxiosRequestConfig (line 12) | interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
  class RequestHttp (line 28) | class RequestHttp {
    method constructor (line 30) | public constructor(config: AxiosRequestConfig) {
    method get (line 102) | get<T>(url: string, params?: object, _object = {}): Promise<ResultData...
    method post (line 105) | post<T>(url: string, params?: object | string, _object = {}): Promise<...
    method put (line 108) | put<T>(url: string, params?: object, _object = {}): Promise<ResultData...
    method delete (line 111) | delete<T>(url: string, params?: any, _object = {}): Promise<ResultData...
    method download (line 114) | download(url: string, params?: object, _object = {}): Promise<BlobPart> {

FILE: src/api/interface/index.ts
  type Result (line 2) | interface Result {
  type ResultData (line 8) | interface ResultData<T = any> extends Result {
  type ResPage (line 13) | interface ResPage<T> {
  type ReqPage (line 21) | interface ReqPage {
  type ResFileUrl (line 28) | interface ResFileUrl {
  type ReqLoginForm (line 35) | interface ReqLoginForm {
  type ResLogin (line 39) | interface ResLogin {
  type ResAuthButtons (line 42) | interface ResAuthButtons {
  type ReqUserParams (line 49) | interface ReqUserParams extends ReqPage {
  type ResUserList (line 58) | interface ResUserList {
  type ResStatus (line 72) | interface ResStatus {
  type ResGender (line 76) | interface ResGender {
  type ResDepartment (line 80) | interface ResDepartment {
  type ResRole (line 85) | interface ResRole {

FILE: src/components/ECharts/config/index.ts
  type ECOption (line 35) | type ECOption = ComposeOption<

FILE: src/components/Grid/interface/index.ts
  type BreakPoint (line 1) | type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl";
  type Responsive (line 3) | type Responsive = {

FILE: src/components/ProTable/interface/index.ts
  type EnumProps (line 7) | interface EnumProps {
  type TypeProps (line 16) | type TypeProps = "index" | "selection" | "radio" | "expand" | "sort";
  type SearchType (line 18) | type SearchType =
  type SearchRenderScope (line 31) | type SearchRenderScope = {
  type SearchProps (line 39) | type SearchProps = {
  type FieldNamesProps (line 52) | type FieldNamesProps = {
  type RenderScope (line 58) | type RenderScope<T> = {
  type HeaderRenderScope (line 65) | type HeaderRenderScope<T> = {
  type ColumnProps (line 71) | interface ColumnProps<T = any>
  type ProTableInstance (line 86) | type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof Compon...

FILE: src/config/index.ts
  constant HOME_URL (line 4) | const HOME_URL: string = "/home/index";
  constant LOGIN_URL (line 7) | const LOGIN_URL: string = "/login";
  constant DEFAULT_PRIMARY (line 10) | const DEFAULT_PRIMARY: string = "#009688";
  constant ROUTER_WHITE_LIST (line 13) | const ROUTER_WHITE_LIST: string[] = ["/500"];
  constant AMAP_MAP_KEY (line 16) | const AMAP_MAP_KEY: string = "";
  constant BAIDU_MAP_KEY (line 19) | const BAIDU_MAP_KEY: string = "";

FILE: src/directives/modules/auth.ts
  method mounted (line 9) | mounted(el: HTMLElement, binding: DirectiveBinding) {

FILE: src/directives/modules/copy.ts
  type ElType (line 9) | interface ElType extends HTMLElement {
  method mounted (line 13) | mounted(el: ElType, binding: DirectiveBinding) {
  method updated (line 17) | updated(el: ElType, binding: DirectiveBinding) {
  method beforeUnmount (line 20) | beforeUnmount(el: ElType) {
  function handleClick (line 25) | async function handleClick(this: any) {

FILE: src/directives/modules/debounce.ts
  type ElType (line 7) | interface ElType extends HTMLElement {
  method mounted (line 11) | mounted(el: ElType, binding: DirectiveBinding) {
  method beforeUnmount (line 26) | beforeUnmount(el: ElType) {

FILE: src/directives/modules/draggable.ts
  type ElType (line 14) | interface ElType extends HTMLElement {

FILE: src/directives/modules/longpress.ts
  method mounted (line 8) | mounted(el: HTMLElement, binding: DirectiveBinding) {

FILE: src/directives/modules/throttle.ts
  type ElType (line 12) | interface ElType extends HTMLElement {
  method mounted (line 17) | mounted(el: ElType, binding: DirectiveBinding) {
  method beforeUnmount (line 36) | beforeUnmount(el: ElType) {

FILE: src/directives/modules/waterMarker.ts
  method mounted (line 31) | mounted(el: DirectiveBinding, binding: DirectiveBinding) {

FILE: src/enums/httpEnum.ts
  type ResultEnum (line 4) | enum ResultEnum {
  type RequestEnum (line 15) | enum RequestEnum {
  type ContentTypeEnum (line 26) | enum ContentTypeEnum {

FILE: src/hooks/interface/index.ts
  type Pageable (line 2) | interface Pageable {
  type StateProps (line 7) | interface StateProps {
  type MessageType (line 26) | type MessageType = "" | "success" | "warning" | "info" | "error";
  type ThemeType (line 30) | type ThemeType = "light" | "inverted" | "dark";
  type GreyOrWeakType (line 31) | type GreyOrWeakType = "grey" | "weak";

FILE: src/stores/interface/index.ts
  type LayoutType (line 1) | type LayoutType = "vertical" | "classic" | "transverse" | "columns";
  type AssemblySizeType (line 3) | type AssemblySizeType = "large" | "default" | "small";
  type LanguageType (line 5) | type LanguageType = "zh" | "en" | null;
  type GlobalState (line 8) | interface GlobalState {
  type UserState (line 30) | interface UserState {
  type TabsMenuProps (line 36) | interface TabsMenuProps {
  type TabsState (line 46) | interface TabsState {
  type AuthState (line 51) | interface AuthState {
  type KeepAliveState (line 60) | interface KeepAliveState {

FILE: src/stores/modules/auth.ts
  method getAuthButtonList (line 30) | async getAuthButtonList() {
  method getAuthMenuList (line 35) | async getAuthMenuList() {
  method setRouteName (line 40) | async setRouteName(name: string) {

FILE: src/stores/modules/global.ts
  method setGlobalState (line 50) | setGlobalState(...args: ObjToKeyValArray<GlobalState>) {

FILE: src/stores/modules/keepAlive.ts
  method addKeepAliveName (line 11) | async addKeepAliveName(name: string) {
  method removeKeepAliveName (line 15) | async removeKeepAliveName(name: string) {
  method setKeepAliveName (line 19) | async setKeepAliveName(keepAliveName: string[] = []) {

FILE: src/stores/modules/tabs.ts
  method addTabs (line 17) | async addTabs(tabItem: TabsMenuProps) {
  method removeTabs (line 27) | async removeTabs(tabPath: string, isCurrent: boolean = true) {
  method closeTabsOnSide (line 43) | async closeTabsOnSide(path: string, type: "left" | "right") {
  method closeMultipleTab (line 56) | async closeMultipleTab(tabsMenuValue?: string) {
  method setTabs (line 65) | async setTabs(tabsMenuList: TabsMenuProps[]) {
  method setTabsTitle (line 69) | async setTabsTitle(title: string) {

FILE: src/stores/modules/user.ts
  method setToken (line 14) | setToken(token: string) {
  method setUserInfo (line 18) | setUserInfo(userInfo: UserState["userInfo"]) {

FILE: src/typings/global.d.ts
  type MenuOptions (line 3) | interface MenuOptions {
  type MetaProps (line 11) | interface MetaProps {
  type ImageMimeType (line 25) | type ImageMimeType =
  type ExcelMimeType (line 37) | type ExcelMimeType = "application/vnd.ms-excel" | "application/vnd.openx...
  type Recordable (line 41) | type Recordable<T = any> = Record<string, T>;
  type ViteEnv (line 43) | interface ViteEnv {
  type ImportMetaEnv (line 61) | interface ImportMetaEnv extends ViteEnv {

FILE: src/typings/utils.d.ts
  type ObjToKeyValUnion (line 1) | type ObjToKeyValUnion<T> = {
  type ObjToKeyValArray (line 5) | type ObjToKeyValArray<T> = {
  type ObjToSelectedValueUnion (line 9) | type ObjToSelectedValueUnion<T> = {
  type Optional (line 13) | type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
  type GetOptional (line 15) | type GetOptional<T> = {

FILE: src/typings/window.d.ts
  type Navigator (line 2) | interface Navigator {

FILE: src/utils/color.ts
  function hexToRgb (line 8) | function hexToRgb(str: any) {
  function rgbToHex (line 25) | function rgbToHex(r: any, g: any, b: any) {
  function getDarkColor (line 39) | function getDarkColor(color: string, level: number) {
  function getLightColor (line 53) | function getLightColor(color: string, level: number) {

FILE: src/utils/eleValidate.ts
  function checkPhoneNumber (line 6) | function checkPhoneNumber(rule: any, value: any, callback: any) {

FILE: src/utils/index.ts
  function localGet (line 11) | function localGet(key: string) {
  function localSet (line 26) | function localSet(key: string, value: any) {
  function localRemove (line 35) | function localRemove(key: string) {
  function localClear (line 43) | function localClear() {
  function isType (line 52) | function isType(val: any) {
  function generateUUID (line 62) | function generateUUID() {
  function isObjectValueEqual (line 78) | function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string...
  function randomNum (line 103) | function randomNum(min: number, max: number): number {
  function getTimeState (line 112) | function getTimeState() {
  function getBrowserLang (line 126) | function getBrowserLang() {
  function getUrlWithParams (line 141) | function getUrlWithParams() {
  function getFlatMenuList (line 154) | function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions...
  function getShowMenuList (line 164) | function getShowMenuList(menuList: Menu.MenuOptions[]) {
  function getMenuListPath (line 193) | function getMenuListPath(menuList: Menu.MenuOptions[], menuPathArr: stri...
  function findMenuByPath (line 207) | function findMenuByPath(menuList: Menu.MenuOptions[], path: string): Men...
  function getKeepAliveRouterName (line 224) | function getKeepAliveRouterName(menuList: Menu.MenuOptions[], keepAliveN...
  function formatTableColumn (line 239) | function formatTableColumn(row: number, col: number, callValue: any) {
  function formatValue (line 250) | function formatValue(callValue: any) {
  function handleRowAccordingToProp (line 262) | function handleRowAccordingToProp(row: { [key: string]: any }, prop: str...
  function handleProp (line 273) | function handleProp(prop: string) {
  function filterEnum (line 287) | function filterEnum(callValue: any, enumData?: any, fieldNames?: FieldNa...
  function findItemNested (line 305) | function findItemNested(enumData: any, callValue: any, value: string, ch...

FILE: src/utils/is/index.ts
  function is (line 4) | function is(val: unknown, type: string) {
  function isFunction (line 11) | function isFunction<T = Function>(val: unknown): val is T {
  function isDate (line 39) | function isDate(val: unknown): val is Date {
  function isNumber (line 46) | function isNumber(val: unknown): val is number {
  function isAsyncFunction (line 53) | function isAsyncFunction<T = any>(val: unknown): val is Promise<T> {
  function isPromise (line 60) | function isPromise<T = any>(val: unknown): val is Promise<T> {
  function isString (line 67) | function isString(val: unknown): val is string {
  function isBoolean (line 74) | function isBoolean(val: unknown): val is boolean {
  function isArray (line 81) | function isArray(val: any): val is Array<any> {
  function isNull (line 109) | function isNull(val: unknown): val is null {
  function isNullOrUnDef (line 116) | function isNullOrUnDef(val: unknown): val is null | undefined {
Condensed preview — 278 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,650K chars).
[
  {
    "path": ".editorconfig",
    "chars": 379,
    "preview": "# @see: http://editorconfig.org\n\nroot = true\n\n[*] # 表示所有文件适用\ncharset = utf-8 # 设置文件字符集为 utf-8\nend_of_line = lf # 控制换行类型("
  },
  {
    "path": ".eslintignore",
    "chars": 111,
    "preview": "*.sh\nnode_modules\n*.md\n*.woff\n*.ttf\n.vscode\n.idea\ndist\n/public\n/docs\n.husky\n.local\n/bin\n/src/mock/*\nstats.html\n"
  },
  {
    "path": ".eslintrc.cjs",
    "chars": 2500,
    "preview": "// @see: http://eslint.cn\n\nmodule.exports = {\n  root: true,\n  env: {\n    browser: true,\n    node: true,\n    es6: true\n  "
  },
  {
    "path": ".gitignore",
    "chars": 287,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": ".husky/commit-msg",
    "chars": 91,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit $1\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 78,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm run lint:lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "chars": 72,
    "preview": "/dist/*\n.local\n/node_modules/**\n\n**/*.svg\n**/*.sh\n\n/public/*\nstats.html\n"
  },
  {
    "path": ".prettierrc.cjs",
    "chars": 1304,
    "preview": "// @see: https://www.prettier.cn\n\nmodule.exports = {\n  // 指定最大换行长度\n  printWidth: 130,\n  // 缩进制表符宽度 | 空格数\n  tabWidth: 2,\n"
  },
  {
    "path": ".stylelintignore",
    "chars": 38,
    "preview": "/dist/*\n/public/*\npublic/*\nstats.html\n"
  },
  {
    "path": ".stylelintrc.cjs",
    "chars": 1595,
    "preview": "// @see: https://stylelint.io\n\nmodule.exports = {\n  root: true,\n  // 继承某些已有的规则\n  extends: [\n    \"stylelint-config-standa"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 301,
    "preview": "{\n  \"recommendations\": [\n    \"vue.volar\",\n    \"hollowtree.vue-snippets\",\n    \"dbaeumer.vscode-eslint\",\n    \"stylelint.vs"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1967,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.stylelint\": \"explicit\"\n  },\n  \"style"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 24870,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github."
  },
  {
    "path": "LICENSE",
    "chars": 1084,
    "preview": "MIT License\r\n\r\nCopyright (c) 2022 Halsey\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 6085,
    "preview": "# Geeker-Admin\n\n### 介绍 📖\n\nGeeker-Admin 一款基于 Vue3.4、TypeScript、Vite5、Pinia、Element-Plus 开源的后台管理框架,使用目前最新技术栈开发。项目提供强大的 [Pr"
  },
  {
    "path": "build/getEnv.ts",
    "chars": 1142,
    "preview": "import path from \"path\";\n\nexport function isDevFn(mode: string): boolean {\n  return mode === \"development\";\n}\n\nexport fu"
  },
  {
    "path": "build/plugins.ts",
    "chars": 3354,
    "preview": "import { resolve } from \"path\";\nimport { PluginOption } from \"vite\";\nimport { VitePWA } from \"vite-plugin-pwa\";\nimport {"
  },
  {
    "path": "build/proxy.ts",
    "chars": 750,
    "preview": "import type { ProxyOptions } from \"vite\";\n\ntype ProxyItem = [string, string];\n\ntype ProxyList = ProxyItem[];\n\ntype Proxy"
  },
  {
    "path": "commitlint.config.cjs",
    "chars": 5287,
    "preview": "// @see: https://cz-git.qbenben.com/zh/guide\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst scopes = fs\n"
  },
  {
    "path": "index.html",
    "chars": 2733,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
  },
  {
    "path": "lint-staged.config.cjs",
    "chars": 387,
    "preview": "module.exports = {\n  \"*.{js,jsx,ts,tsx}\": [\"eslint --fix\", \"prettier --write\"],\n  \"{!(package)*.json,*.code-snippets,.!("
  },
  {
    "path": "package.json",
    "chars": 3902,
    "preview": "{\n  \"name\": \"geeker-admin\",\n  \"private\": true,\n  \"version\": \"1.2.0\",\n  \"type\": \"module\",\n  \"description\": \"geeker-admin "
  },
  {
    "path": "postcss.config.cjs",
    "chars": 60,
    "preview": "module.exports = {\n  plugins: {\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "src/App.vue",
    "chars": 1335,
    "preview": "<template>\n  <el-config-provider :locale=\"locale\" :size=\"assemblySize\" :button=\"buttonConfig\">\n    <router-view></router"
  },
  {
    "path": "src/api/config/servicePort.ts",
    "chars": 76,
    "preview": "// 后端微服务模块前缀\nexport const PORT1 = \"/geeker\";\nexport const PORT2 = \"/hooks\";\n"
  },
  {
    "path": "src/api/helper/axiosCancel.ts",
    "chars": 1409,
    "preview": "import { CustomAxiosRequestConfig } from \"../index\";\nimport qs from \"qs\";\n\n// 声明一个 Map 用于存储每个请求的标识和取消函数\nlet pendingMap ="
  },
  {
    "path": "src/api/helper/checkStatus.ts",
    "chars": 869,
    "preview": "import { ElMessage } from \"element-plus\";\n\n/**\n * @description: 校验网络请求状态码\n * @param {Number} status\n * @return void\n */\n"
  },
  {
    "path": "src/api/index.ts",
    "chars": 4216,
    "preview": "import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from \"axios\";"
  },
  {
    "path": "src/api/interface/index.ts",
    "chars": 1614,
    "preview": "// 请求响应参数(不包含data)\nexport interface Result {\n  code: string;\n  msg: string;\n}\n\n// 请求响应参数(包含data)\nexport interface Result"
  },
  {
    "path": "src/api/modules/login.ts",
    "chars": 1499,
    "preview": "import { Login } from \"@/api/interface/index\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport authMenuList fro"
  },
  {
    "path": "src/api/modules/upload.ts",
    "chars": 462,
    "preview": "import { Upload } from \"@/api/interface/index\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport http from \"@/ap"
  },
  {
    "path": "src/api/modules/user.ts",
    "chars": 1839,
    "preview": "import { ResPage, User } from \"@/api/interface/index\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport http fro"
  },
  {
    "path": "src/assets/fonts/font.scss",
    "chars": 214,
    "preview": "@font-face {\n  font-family: YouSheBiaoTiHei;\n  src: url(\"./YouSheBiaoTiHei.ttf\");\n}\n\n@font-face {\n  font-family: MetroDF"
  },
  {
    "path": "src/assets/iconfont/iconfont.scss",
    "chars": 931,
    "preview": "@font-face {\n  font-family: iconfont; /* Project id 2667653 */\n  src: url(\"iconfont.ttf?t=1719667796161\") format(\"truety"
  },
  {
    "path": "src/assets/json/authButtonList.json",
    "chars": 189,
    "preview": "{\n  \"code\": 200,\n  \"data\": {\n    \"useProTable\": [\"add\", \"batchAdd\", \"export\", \"batchDelete\", \"status\"],\n    \"authButton\""
  },
  {
    "path": "src/assets/json/authMenuList.json",
    "chars": 27495,
    "preview": "{\n  \"code\": 200,\n  \"data\": [\n    {\n      \"path\": \"/home/index\",\n      \"name\": \"home\",\n      \"component\": \"/home/index\",\n"
  },
  {
    "path": "src/components/ECharts/config/index.ts",
    "chars": 1588,
    "preview": "import * as echarts from \"echarts/core\";\nimport { BarChart, LineChart, LinesChart, PieChart, ScatterChart, RadarChart, G"
  },
  {
    "path": "src/components/ECharts/index.vue",
    "chars": 2519,
    "preview": "<template>\n  <div id=\"echarts\" ref=\"chartRef\" :style=\"echartsStyle\" />\n</template>\n\n<script setup lang=\"ts\" name=\"EChart"
  },
  {
    "path": "src/components/ErrorMessage/403.vue",
    "chars": 474,
    "preview": "<template>\n  <div class=\"not-container\">\n    <img src=\"@/assets/images/403.png\" class=\"not-img\" alt=\"403\" />\n    <div cl"
  },
  {
    "path": "src/components/ErrorMessage/404.vue",
    "chars": 475,
    "preview": "<template>\n  <div class=\"not-container\">\n    <img src=\"@/assets/images/404.png\" class=\"not-img\" alt=\"404\" />\n    <div cl"
  },
  {
    "path": "src/components/ErrorMessage/500.vue",
    "chars": 473,
    "preview": "<template>\n  <div class=\"not-container\">\n    <img src=\"@/assets/images/500.png\" class=\"not-img\" alt=\"500\" />\n    <div cl"
  },
  {
    "path": "src/components/ErrorMessage/index.scss",
    "chars": 543,
    "preview": ".not-container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 100%;\n  .no"
  },
  {
    "path": "src/components/Grid/components/GridItem.vue",
    "chars": 1828,
    "preview": "<template>\n  <div v-show=\"isShow\" :style=\"style\">\n    <slot></slot>\n  </div>\n</template>\n<script setup lang=\"ts\" name=\"G"
  },
  {
    "path": "src/components/Grid/index.vue",
    "chars": 4090,
    "preview": "<template>\n  <div :style=\"style\">\n    <slot></slot>\n  </div>\n</template>\n\n<script setup lang=\"ts\" name=\"Grid\">\nimport {\n"
  },
  {
    "path": "src/components/Grid/interface/index.ts",
    "chars": 126,
    "preview": "export type BreakPoint = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nexport type Responsive = {\n  span?: number;\n  offset?: numbe"
  },
  {
    "path": "src/components/ImportExcel/index.scss",
    "chars": 26,
    "preview": ".upload {\n  width: 80%;\n}\n"
  },
  {
    "path": "src/components/ImportExcel/index.vue",
    "chars": 4184,
    "preview": "<template>\n  <el-dialog v-model=\"dialogVisible\" :title=\"`批量添加${parameter.title}`\" :destroy-on-close=\"true\" width=\"580px\""
  },
  {
    "path": "src/components/Loading/fullScreen.ts",
    "chars": 854,
    "preview": "import { ElLoading } from \"element-plus\";\n\n/* 全局请求 loading */\nlet loadingInstance: ReturnType<typeof ElLoading.service>;"
  },
  {
    "path": "src/components/Loading/index.scss",
    "chars": 1128,
    "preview": ".loading-box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 100"
  },
  {
    "path": "src/components/Loading/index.vue",
    "chars": 282,
    "preview": "<template>\n  <div class=\"loading-box\">\n    <div class=\"loading-wrap\">\n      <span class=\"dot dot-spin\"><i></i><i></i><i>"
  },
  {
    "path": "src/components/ProTable/components/ColSetting.vue",
    "chars": 1312,
    "preview": "<template>\n  <!-- 列设置 -->\n  <el-drawer v-model=\"drawerVisible\" title=\"列设置\" size=\"450px\">\n    <div class=\"table-main\">\n  "
  },
  {
    "path": "src/components/ProTable/components/Pagination.vue",
    "chars": 831,
    "preview": "<template>\n  <!-- 分页组件 -->\n  <el-pagination\n    :background=\"true\"\n    :current-page=\"pageable.pageNum\"\n    :page-size=\""
  },
  {
    "path": "src/components/ProTable/components/TableColumn.vue",
    "chars": 2176,
    "preview": "<template>\n  <RenderTableColumn v-bind=\"column\" />\n</template>\n\n<script setup lang=\"tsx\" name=\"TableColumn\">\nimport { in"
  },
  {
    "path": "src/components/ProTable/index.vue",
    "chars": 10086,
    "preview": "<!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 -->\n\n<template>\n  <!-- 查询表单 -->\n  <SearchForm\n    v-sh"
  },
  {
    "path": "src/components/ProTable/interface/index.ts",
    "chars": 2827,
    "preview": "import { VNode, ComponentPublicInstance, Ref } from \"vue\";\nimport { BreakPoint, Responsive } from \"@/components/Grid/int"
  },
  {
    "path": "src/components/SearchForm/components/SearchFormItem.vue",
    "chars": 3487,
    "preview": "<template>\n  <component\n    :is=\"column.search?.render ?? `el-${column.search?.el}`\"\n    v-bind=\"{ ...handleSearchProps,"
  },
  {
    "path": "src/components/SearchForm/index.vue",
    "chars": 3294,
    "preview": "<template>\n  <div v-if=\"columns.length\" class=\"card table-search\">\n    <el-form ref=\"formRef\" :model=\"searchParam\">\n    "
  },
  {
    "path": "src/components/SelectFilter/index.scss",
    "chars": 1542,
    "preview": ".select-filter {\n  width: 100%;\n  .select-filter-item {\n    display: flex;\n    align-items: center;\n    border-bottom: 1"
  },
  {
    "path": "src/components/SelectFilter/index.vue",
    "chars": 3142,
    "preview": "<template>\n  <div class=\"select-filter\">\n    <div v-for=\"item in data\" :key=\"item.key\" class=\"select-filter-item\">\n     "
  },
  {
    "path": "src/components/SelectIcon/index.scss",
    "chars": 840,
    "preview": ".icon-box {\n  width: 100%;\n  .el-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    f"
  },
  {
    "path": "src/components/SelectIcon/index.vue",
    "chars": 2428,
    "preview": "<template>\n  <div class=\"icon-box\">\n    <el-input\n      ref=\"inputRef\"\n      v-model=\"valueIcon\"\n      v-bind=\"$attrs\"\n "
  },
  {
    "path": "src/components/SvgIcon/index.vue",
    "chars": 562,
    "preview": "<template>\n  <svg :style=\"iconStyle\" aria-hidden=\"true\">\n    <use :xlink:href=\"symbolId\" />\n  </svg>\n</template>\n\n<scrip"
  },
  {
    "path": "src/components/SwitchDark/index.vue",
    "chars": 432,
    "preview": "<template>\n  <el-switch v-model=\"globalStore.isDark\" inline-prompt :active-icon=\"Sunny\" :inactive-icon=\"Moon\" @change=\"s"
  },
  {
    "path": "src/components/TreeFilter/index.scss",
    "chars": 908,
    "preview": ".filter {\n  box-sizing: border-box;\n  width: 220px;\n  height: 100%;\n  padding: 18px;\n  margin-right: 10px;\n  .title {\n  "
  },
  {
    "path": "src/components/TreeFilter/index.vue",
    "chars": 4450,
    "preview": "<template>\n  <div class=\"card filter\">\n    <h4 v-if=\"title\" class=\"title sle\">\n      {{ title }}\n    </h4>\n    <div clas"
  },
  {
    "path": "src/components/Upload/Img.vue",
    "chars": 7968,
    "preview": "<template>\n  <div class=\"upload-box\">\n    <el-upload\n      :id=\"uuid\"\n      action=\"#\"\n      :class=\"['upload', self_dis"
  },
  {
    "path": "src/components/Upload/Imgs.vue",
    "chars": 8205,
    "preview": "<template>\n  <div class=\"upload-box\">\n    <el-upload\n      v-model:file-list=\"_fileList\"\n      action=\"#\"\n      list-typ"
  },
  {
    "path": "src/components/WangEditor/index.scss",
    "chars": 522,
    "preview": "/* 富文本组件校验失败样式 */\n.is-error {\n  .editor-box {\n    border-color: var(--el-color-danger);\n    .editor-toolbar {\n      bord"
  },
  {
    "path": "src/components/WangEditor/index.vue",
    "chars": 4070,
    "preview": "<template>\n  <div :class=\"['editor-box', self_disabled ? 'editor-disabled' : '']\">\n    <Toolbar v-if=\"!hideToolBar\" clas"
  },
  {
    "path": "src/config/index.ts",
    "chars": 390,
    "preview": "// ? 全局默认配置项\n\n// 首页地址(默认)\nexport const HOME_URL: string = \"/home/index\";\n\n// 登录页地址(默认)\nexport const LOGIN_URL: string = "
  },
  {
    "path": "src/config/nprogress.ts",
    "chars": 269,
    "preview": "import NProgress from \"nprogress\";\nimport \"nprogress/nprogress.css\";\n\nNProgress.configure({\n  easing: \"ease\", // 动画方式\n  "
  },
  {
    "path": "src/directives/index.ts",
    "chars": 675,
    "preview": "import { App, Directive } from \"vue\";\nimport auth from \"./modules/auth\";\nimport copy from \"./modules/copy\";\nimport water"
  },
  {
    "path": "src/directives/modules/auth.ts",
    "chars": 652,
    "preview": "/**\n * v-auth\n * 按钮权限指令\n */\nimport { useAuthStore } from \"@/stores/modules/auth\";\nimport type { Directive, DirectiveBind"
  },
  {
    "path": "src/directives/modules/copy.ts",
    "chars": 849,
    "preview": "/**\n * v-copy\n * 复制某个值至剪贴板\n * 接收参数:string类型/Ref<string>类型/Reactive<string>类型\n */\n\nimport type { Directive, DirectiveBind"
  },
  {
    "path": "src/directives/modules/debounce.ts",
    "chars": 767,
    "preview": "/**\n * v-debounce\n * 按钮防抖指令,可自行扩展至input\n * 接收参数:function类型\n */\nimport type { Directive, DirectiveBinding } from \"vue\";\ni"
  },
  {
    "path": "src/directives/modules/draggable.ts",
    "chars": 1298,
    "preview": "/*\n\t需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。\n\n\t思路:\n\t\t1、设置需要拖拽的元素为absolute,其父元素为relative。\n\t\t2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。"
  },
  {
    "path": "src/directives/modules/longpress.ts",
    "chars": 1196,
    "preview": "/**\n * v-longpress\n * 长按指令,长按时触发事件\n */\nimport type { Directive, DirectiveBinding } from \"vue\";\n\nconst directive: Directi"
  },
  {
    "path": "src/directives/modules/throttle.ts",
    "chars": 1020,
    "preview": "/*\n  需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。\n\n  思路:\n    1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮\n    2、将需要触发的方法绑定在指令上\n  \n  使用:给 Do"
  },
  {
    "path": "src/directives/modules/waterMarker.ts",
    "chars": 1197,
    "preview": "/*\n  需求:给整个页面添加背景水印。\n\n  思路:\n    1、使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。\n    2、将其设置为背景图片,从而实现页面或组件水印效果\n  \n  使用:设置水印文"
  },
  {
    "path": "src/enums/httpEnum.ts",
    "chars": 623,
    "preview": "/**\n * @description:请求配置\n */\nexport enum ResultEnum {\n  SUCCESS = 200,\n  ERROR = 500,\n  OVERDUE = 401,\n  TIMEOUT = 30000"
  },
  {
    "path": "src/hooks/interface/index.ts",
    "chars": 651,
    "preview": "export namespace Table {\n  export interface Pageable {\n    pageNum: number;\n    pageSize: number;\n    total: number;\n  }"
  },
  {
    "path": "src/hooks/useAuthButtons.ts",
    "chars": 583,
    "preview": "import { computed } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport { useAuthStore } from \"@/stores/modules/au"
  },
  {
    "path": "src/hooks/useDownload.ts",
    "chars": 1320,
    "preview": "import { ElNotification } from \"element-plus\";\n\n/**\n * @description 接收数据流生成 blob,创建链接,下载文件\n * @param {Function} api 导出表格"
  },
  {
    "path": "src/hooks/useHandleData.ts",
    "chars": 1040,
    "preview": "import { ElMessageBox, ElMessage } from \"element-plus\";\nimport { HandleData } from \"./interface\";\n\n/**\n * @description 操"
  },
  {
    "path": "src/hooks/useOnline.ts",
    "chars": 670,
    "preview": "import { ref, onMounted, onUnmounted } from \"vue\";\n\n/**\n * @description 网络是否可用\n * */\nexport const useOnline = () => {\n  "
  },
  {
    "path": "src/hooks/useSelection.ts",
    "chars": 827,
    "preview": "import { ref, computed } from \"vue\";\n\n/**\n * @description 表格多选数据操作\n * @param {String} rowKey 当表格可以多选时,所指定的 id\n * */\nexpo"
  },
  {
    "path": "src/hooks/useTable.ts",
    "chars": 3342,
    "preview": "import { Table } from \"./interface\";\nimport { reactive, computed, toRefs } from \"vue\";\n\n/**\n * @description table 页面操作方法"
  },
  {
    "path": "src/hooks/useTheme.ts",
    "chars": 3670,
    "preview": "import { storeToRefs } from \"pinia\";\nimport { Theme } from \"./interface\";\nimport { ElMessage } from \"element-plus\";\nimpo"
  },
  {
    "path": "src/hooks/useTime.ts",
    "chars": 1316,
    "preview": "import { ref } from \"vue\";\n\n/**\n * @description 获取本地时间\n */\nexport const useTime = () => {\n  const year = ref(0); // 年份\n "
  },
  {
    "path": "src/languages/index.ts",
    "chars": 340,
    "preview": "import { createI18n } from \"vue-i18n\";\nimport { getBrowserLang } from \"@/utils\";\n\nimport zh from \"./modules/zh\";\nimport "
  },
  {
    "path": "src/languages/modules/en.ts",
    "chars": 680,
    "preview": "export default {\n  home: {\n    welcome: \"Welcome\"\n  },\n  tabs: {\n    refresh: \"Refresh\",\n    maximize: \"Maximize\",\n    c"
  },
  {
    "path": "src/languages/modules/zh.ts",
    "chars": 550,
    "preview": "export default {\n  home: {\n    welcome: \"欢迎使用\"\n  },\n  tabs: {\n    refresh: \"刷新\",\n    maximize: \"最大化\",\n    closeCurrent: "
  },
  {
    "path": "src/layouts/LayoutClassic/index.scss",
    "chars": 1398,
    "preview": ".el-container {\n  width: 100%;\n  height: 100%;\n  :deep(.el-header) {\n    box-sizing: border-box;\n    display: flex;\n    "
  },
  {
    "path": "src/layouts/LayoutClassic/index.vue",
    "chars": 2072,
    "preview": "<!-- 经典布局 -->\n<template>\n  <el-container class=\"layout\">\n    <el-header>\n      <div class=\"header-lf mask-image\">\n      "
  },
  {
    "path": "src/layouts/LayoutColumns/index.scss",
    "chars": 2187,
    "preview": ".el-container {\n  width: 100%;\n  height: 100%;\n  .aside-split {\n    display: flex;\n    flex-direction: column;\n    flex-"
  },
  {
    "path": "src/layouts/LayoutColumns/index.vue",
    "chars": 3370,
    "preview": "<!-- 分栏布局 -->\n<template>\n  <el-container class=\"layout\">\n    <div class=\"aside-split\">\n      <div class=\"logo flx-center"
  },
  {
    "path": "src/layouts/LayoutTransverse/index.scss",
    "chars": 1432,
    "preview": ".el-container {\n  width: 100%;\n  height: 100%;\n  :deep(.el-header) {\n    box-sizing: border-box;\n    display: flex;\n    "
  },
  {
    "path": "src/layouts/LayoutTransverse/index.vue",
    "chars": 2268,
    "preview": "<!-- 横向布局 -->\n<template>\n  <el-container class=\"layout\">\n    <el-header>\n      <div class=\"logo flx-center\">\n        <im"
  },
  {
    "path": "src/layouts/LayoutVertical/index.scss",
    "chars": 1135,
    "preview": ".el-container {\n  width: 100%;\n  height: 100%;\n  :deep(.el-aside) {\n    width: auto;\n    background-color: var(--el-menu"
  },
  {
    "path": "src/layouts/LayoutVertical/index.vue",
    "chars": 1881,
    "preview": "<!-- 纵向布局 -->\n<template>\n  <el-container class=\"layout\">\n    <el-aside>\n      <div class=\"aside-box\" :style=\"{ width: is"
  },
  {
    "path": "src/layouts/components/Footer/index.scss",
    "chars": 250,
    "preview": ".footer {\n  height: 30px;\n  background-color: var(--el-bg-color);\n  border-top: 1px solid var(--el-border-color-light);\n"
  },
  {
    "path": "src/layouts/components/Footer/index.vue",
    "chars": 236,
    "preview": "<template>\n  <div class=\"footer flx-center\">\n    <a href=\"https://github.com/HalseySpicy\" target=\"_blank\"> 2022 © Geeker"
  },
  {
    "path": "src/layouts/components/Header/ToolBarLeft.vue",
    "chars": 574,
    "preview": "<template>\n  <div class=\"tool-bar-lf\">\n    <CollapseIcon id=\"collapseIcon\" />\n    <Breadcrumb v-show=\"globalStore.breadc"
  },
  {
    "path": "src/layouts/components/Header/ToolBarRight.vue",
    "chars": 1361,
    "preview": "<template>\n  <div class=\"tool-bar-ri\">\n    <div class=\"header-icon\">\n      <AssemblySize id=\"assemblySize\" />\n      <Lan"
  },
  {
    "path": "src/layouts/components/Header/components/AssemblySize.vue",
    "chars": 1079,
    "preview": "<template>\n  <el-dropdown trigger=\"click\" @command=\"setAssemblySize\">\n    <i :class=\"'iconfont icon-contentright'\" class"
  },
  {
    "path": "src/layouts/components/Header/components/Avatar.vue",
    "chars": 2190,
    "preview": "<template>\n  <el-dropdown trigger=\"click\">\n    <div class=\"avatar\">\n      <img src=\"@/assets/images/avatar.gif\" alt=\"ava"
  },
  {
    "path": "src/layouts/components/Header/components/Breadcrumb.vue",
    "chars": 2997,
    "preview": "<template>\n  <div :class=\"['breadcrumb-box mask-image', !globalStore.breadcrumbIcon && 'no-icon']\">\n    <el-breadcrumb :"
  },
  {
    "path": "src/layouts/components/Header/components/CollapseIcon.vue",
    "chars": 554,
    "preview": "<template>\n  <el-icon class=\"collapse-icon\" @click=\"changeCollapse\">\n    <component :is=\"globalStore.isCollapse ? 'expan"
  },
  {
    "path": "src/layouts/components/Header/components/Fullscreen.vue",
    "chars": 678,
    "preview": "<template>\n  <div class=\"fullscreen\">\n    <i :class=\"['iconfont', isFullscreen ? 'icon-suoxiao' : 'icon-fangda']\" class="
  },
  {
    "path": "src/layouts/components/Header/components/InfoDialog.vue",
    "chars": 569,
    "preview": "<template>\n  <el-dialog v-model=\"dialogVisible\" title=\"个人信息\" width=\"500px\" draggable>\n    <span>This is userInfo</span>\n"
  },
  {
    "path": "src/layouts/components/Header/components/Language.vue",
    "chars": 1064,
    "preview": "<template>\n  <el-dropdown trigger=\"click\" @command=\"changeLanguage\">\n    <i :class=\"'iconfont icon-zhongyingwen'\" class="
  },
  {
    "path": "src/layouts/components/Header/components/Message.vue",
    "chars": 3469,
    "preview": "<template>\n  <div class=\"message\">\n    <el-popover placement=\"bottom\" :width=\"310\" trigger=\"click\">\n      <template #ref"
  },
  {
    "path": "src/layouts/components/Header/components/PasswordDialog.vue",
    "chars": 569,
    "preview": "<template>\n  <el-dialog v-model=\"dialogVisible\" title=\"修改密码\" width=\"500px\" draggable>\n    <span>This is Password</span>\n"
  },
  {
    "path": "src/layouts/components/Header/components/SearchMenu.vue",
    "chars": 5258,
    "preview": "<template>\n  <div class=\"search-menu\">\n    <i :class=\"'iconfont icon-sousuo'\" class=\"toolBar-icon\" @click=\"handleOpen\"><"
  },
  {
    "path": "src/layouts/components/Header/components/ThemeSetting.vue",
    "chars": 286,
    "preview": "<template>\n  <div class=\"theme-setting\">\n    <i :class=\"'iconfont icon-zhuti'\" class=\"toolBar-icon\" @click=\"openDrawer\">"
  },
  {
    "path": "src/layouts/components/Main/components/Maximize.vue",
    "chars": 762,
    "preview": "<template>\n  <div class=\"maximize\" @click=\"exitMaximize\">\n    <i :class=\"'iconfont icon-tuichu'\"></i>\n  </div>\n</templat"
  },
  {
    "path": "src/layouts/components/Main/index.scss",
    "chars": 173,
    "preview": ".el-main {\n  box-sizing: border-box;\n  padding: 10px 12px;\n  overflow-x: hidden;\n  background-color: var(--el-bg-color-p"
  },
  {
    "path": "src/layouts/components/Main/index.vue",
    "chars": 2739,
    "preview": "<template>\n  <Maximize v-show=\"maximize\" />\n  <Tabs v-show=\"tabs\" />\n  <el-main>\n    <router-view v-slot=\"{ Component, r"
  },
  {
    "path": "src/layouts/components/Menu/SubMenu.vue",
    "chars": 2040,
    "preview": "<template>\n  <template v-for=\"subItem in menuList\" :key=\"subItem.path\">\n    <el-sub-menu v-if=\"subItem.children?.length\""
  },
  {
    "path": "src/layouts/components/Tabs/components/MoreButton.vue",
    "chars": 2733,
    "preview": "<template>\n  <el-dropdown trigger=\"click\" :teleported=\"false\">\n    <div class=\"more-button\">\n      <i :class=\"'iconfont "
  },
  {
    "path": "src/layouts/components/Tabs/index.scss",
    "chars": 1737,
    "preview": ".tabs-box {\n  background-color: var(--el-bg-color);\n  .tabs-menu {\n    position: relative;\n    width: 100%;\n    .el-drop"
  },
  {
    "path": "src/layouts/components/Tabs/index.vue",
    "chars": 3118,
    "preview": "<template>\n  <div class=\"tabs-box\">\n    <div class=\"tabs-menu\">\n      <el-tabs v-model=\"tabsMenuValue\" type=\"card\" @tab-"
  },
  {
    "path": "src/layouts/components/ThemeDrawer/index.scss",
    "chars": 2772,
    "preview": ".divider {\n  margin-top: 15px;\n  .el-icon {\n    position: relative;\n    top: 2px;\n    right: 5px;\n    font-size: 15px;\n "
  },
  {
    "path": "src/layouts/components/ThemeDrawer/index.vue",
    "chars": 5707,
    "preview": "<template>\n  <el-drawer v-model=\"drawerVisible\" title=\"布局设置\" size=\"290px\">\n    <!-- 布局样式 -->\n    <el-divider class=\"divi"
  },
  {
    "path": "src/layouts/index.vue",
    "chars": 1389,
    "preview": "<!-- 💥 这里是一次性加载 LayoutComponents -->\n<template>\n  <el-watermark id=\"watermark\" :font=\"font\" :content=\"watermark ? ['Geek"
  },
  {
    "path": "src/layouts/indexAsync.vue",
    "chars": 1585,
    "preview": "<!-- 💥 这里是异步加载 LayoutComponents -->\n<template>\n  <el-watermark id=\"watermark\" :font=\"font\" :content=\"watermark ? ['Geeke"
  },
  {
    "path": "src/main.ts",
    "chars": 1257,
    "preview": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\n// reset style sheet\nimport \"@/styles/reset.scss\";\n// CSS "
  },
  {
    "path": "src/routers/index.ts",
    "chars": 2839,
    "preview": "import { createRouter, createWebHashHistory, createWebHistory } from \"vue-router\";\nimport { useUserStore } from \"@/store"
  },
  {
    "path": "src/routers/modules/dynamicRouter.ts",
    "chars": 1553,
    "preview": "import router from \"@/routers/index\";\nimport { LOGIN_URL } from \"@/config\";\nimport { RouteRecordRaw } from \"vue-router\";"
  },
  {
    "path": "src/routers/modules/staticRouter.ts",
    "chars": 1223,
    "preview": "import { RouteRecordRaw } from \"vue-router\";\nimport { HOME_URL, LOGIN_URL } from \"@/config\";\n\n/**\n * staticRouter (静态路由)"
  },
  {
    "path": "src/stores/helper/persist.ts",
    "chars": 454,
    "preview": "import { PersistedStateOptions } from \"pinia-plugin-persistedstate\";\n\n/**\n * @description pinia 持久化参数配置\n * @param {Strin"
  },
  {
    "path": "src/stores/index.ts",
    "chars": 214,
    "preview": "import { createPinia } from \"pinia\";\nimport piniaPluginPersistedstate from \"pinia-plugin-persistedstate\";\n\n// pinia pers"
  },
  {
    "path": "src/stores/interface/index.ts",
    "chars": 1228,
    "preview": "export type LayoutType = \"vertical\" | \"classic\" | \"transverse\" | \"columns\";\n\nexport type AssemblySizeType = \"large\" | \"d"
  },
  {
    "path": "src/stores/modules/auth.ts",
    "chars": 1371,
    "preview": "import { defineStore } from \"pinia\";\nimport { AuthState } from \"@/stores/interface\";\nimport { getAuthButtonListApi, getA"
  },
  {
    "path": "src/stores/modules/global.ts",
    "chars": 1235,
    "preview": "import { defineStore } from \"pinia\";\nimport { GlobalState } from \"@/stores/interface\";\nimport { DEFAULT_PRIMARY } from \""
  },
  {
    "path": "src/stores/modules/keepAlive.ts",
    "chars": 687,
    "preview": "import { defineStore } from \"pinia\";\nimport { KeepAliveState } from \"@/stores/interface\";\n\nexport const useKeepAliveStor"
  },
  {
    "path": "src/stores/modules/tabs.ts",
    "chars": 2913,
    "preview": "import router from \"@/routers\";\nimport { defineStore } from \"pinia\";\nimport { getUrlWithParams } from \"@/utils\";\nimport "
  },
  {
    "path": "src/stores/modules/user.ts",
    "chars": 564,
    "preview": "import { defineStore } from \"pinia\";\nimport { UserState } from \"@/stores/interface\";\nimport piniaPersistConfig from \"@/s"
  },
  {
    "path": "src/styles/common.scss",
    "chars": 2175,
    "preview": "/* flex */\n.flx-center {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.flx-justify-between {\n  d"
  },
  {
    "path": "src/styles/element-dark.scss",
    "chars": 727,
    "preview": "/* 自定义 element 暗黑模式 */\nhtml.dark {\n  /* wangEditor */\n  --w-e-toolbar-color: #eeeeee;\n  --w-e-toolbar-bg-color: #141414;"
  },
  {
    "path": "src/styles/element.scss",
    "chars": 4827,
    "preview": "/* el-alert */\n.el-alert {\n  border: 1px solid;\n}\n\n/* 当前页面最大化 css */\n.main-maximize {\n  .aside-split,\n  .el-aside,\n  .el"
  },
  {
    "path": "src/styles/reset.scss",
    "chars": 1647,
    "preview": "/* Reset style sheet */\n\n/* 目前项目中使用富文本编辑器需要注释,如果你项目中没有使用富文本编辑器,可以取消注释 */\n// html,\n// body,\n// div,\n// span,\n// applet,\n/"
  },
  {
    "path": "src/styles/theme/aside.ts",
    "chars": 434,
    "preview": "import { Theme } from \"@/hooks/interface\";\n\nexport const asideTheme: Record<Theme.ThemeType, { [key: string]: string }> "
  },
  {
    "path": "src/styles/theme/header.ts",
    "chars": 828,
    "preview": "import { Theme } from \"@/hooks/interface\";\n\nexport const headerTheme: Record<Theme.ThemeType, { [key: string]: string }>"
  },
  {
    "path": "src/styles/theme/menu.ts",
    "chars": 1115,
    "preview": "import { Theme } from \"@/hooks/interface\";\n\nexport const menuTheme: Record<Theme.ThemeType, { [key: string]: string }> ="
  },
  {
    "path": "src/styles/var.scss",
    "chars": 67,
    "preview": "/* global css variable */\n$primary-color: var(--el-color-primary);\n"
  },
  {
    "path": "src/typings/global.d.ts",
    "chars": 1690,
    "preview": "/* Menu */\ndeclare namespace Menu {\n  interface MenuOptions {\n    path: string;\n    name: string;\n    component?: string"
  },
  {
    "path": "src/typings/utils.d.ts",
    "chars": 396,
    "preview": "type ObjToKeyValUnion<T> = {\n  [K in keyof T]: { key: K; value: T[K] };\n}[keyof T];\n\ntype ObjToKeyValArray<T> = {\n  [K i"
  },
  {
    "path": "src/typings/window.d.ts",
    "chars": 150,
    "preview": "declare global {\n  interface Navigator {\n    msSaveOrOpenBlob: (blob: Blob, fileName: string) => void;\n    browserLangua"
  },
  {
    "path": "src/utils/color.ts",
    "chars": 1816,
    "preview": "import { ElMessage } from \"element-plus\";\n\n/**\n * @description hex颜色转rgb颜色\n * @param {String} str 颜色值字符串\n * @returns {St"
  },
  {
    "path": "src/utils/dict.ts",
    "chars": 285,
    "preview": "// ? 系统全局字典\n\n/**\n * @description:用户性别\n */\nexport const genderType = [\n  { label: \"男\", value: 1 },\n  { label: \"女\", value:"
  },
  {
    "path": "src/utils/eleValidate.ts",
    "chars": 390,
    "preview": "// ? Element 常用表单校验规则\n\n/**\n *  @rule 手机号\n */\nexport function checkPhoneNumber(rule: any, value: any, callback: any) {\n  "
  },
  {
    "path": "src/utils/errorHandler.ts",
    "chars": 641,
    "preview": "import { ElNotification } from \"element-plus\";\n\n/**\n * @description 全局代码错误捕捉\n * */\nconst errorHandler = (error: any) => "
  },
  {
    "path": "src/utils/index.ts",
    "chars": 8949,
    "preview": "import { isArray } from \"@/utils/is\";\nimport { FieldNamesProps } from \"@/components/ProTable/interface\";\n\nconst mode = i"
  },
  {
    "path": "src/utils/is/index.ts",
    "chars": 2470,
    "preview": "/**\n * @description: 判断值是否未某个类型\n */\nexport function is(val: unknown, type: string) {\n  return Object.prototype.toString."
  },
  {
    "path": "src/utils/mittBus.ts",
    "chars": 75,
    "preview": "import mitt from \"mitt\";\n\nconst mittBus = mitt();\n\nexport default mittBus;\n"
  },
  {
    "path": "src/utils/svg.ts",
    "chars": 240,
    "preview": "/**\n * @description Loading Svg\n */\nexport const loadingSvg = `\n<path class=\"path\" d=\"\n\tM 30 15\n\tL 28 17\n\tM 25.61 25.61\n"
  },
  {
    "path": "src/views/about/index.vue",
    "chars": 2936,
    "preview": "<template>\n  <div>\n    <div class=\"card mb10\">\n      <h4 class=\"title\">简介</h4>\n      <span class=\"text\">\n        <el-lin"
  },
  {
    "path": "src/views/assembly/batchImport/index.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/views/assembly/batchImport/index.vue",
    "chars": 1460,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\">批量添加数据 🍓🍇🍈🍉</span>\n    <el-button type=\"primary\" :ico"
  },
  {
    "path": "src/views/assembly/draggable/index.scss",
    "chars": 764,
    "preview": ".grid-container {\n  display: grid;\n  grid-template-rows: 33.3% 33.3% 33.3%;\n  grid-template-columns: 33.3% 33.3% 33.3%;\n"
  },
  {
    "path": "src/views/assembly/draggable/index.vue",
    "chars": 741,
    "preview": "<template>\n  <draggable\n    v-model=\"gridList\"\n    class=\"card grid-container\"\n    item-key=\"id\"\n    animation=\"300\"\n   "
  },
  {
    "path": "src/views/assembly/guide/index.scss",
    "chars": 35,
    "preview": ".el-button {\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "src/views/assembly/guide/index.vue",
    "chars": 2089,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 引导页 🍓🍇🍈🍉</span>\n    <el-alert\n      title=\"引导页对于一些第一"
  },
  {
    "path": "src/views/assembly/selectFilter/index.scss",
    "chars": 111,
    "preview": ".result {\n  margin-top: 20px;\n  font-size: 17px;\n  font-weight: bold;\n  color: var(--el-text-color-regular);\n}\n"
  },
  {
    "path": "src/views/assembly/selectFilter/index.vue",
    "chars": 1802,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 分类筛选器 🍓🍇🍈🍉</span>\n    <SelectFilter :data=\"filterDat"
  },
  {
    "path": "src/views/assembly/selectIcon/index.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/views/assembly/selectIcon/index.vue",
    "chars": 821,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 图标选择器 🍓🍇🍈🍉</span>\n    <SelectIcon v-model:icon-value"
  },
  {
    "path": "src/views/assembly/svgIcon/index.scss",
    "chars": 151,
    "preview": ".icon-list {\n  box-sizing: border-box;\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n  width: 10"
  },
  {
    "path": "src/views/assembly/svgIcon/index.vue",
    "chars": 1298,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <el-alert\n      title=\"SVG 图标目前使用 vite-plugin-svg-icons 插件完成,官方文档请查看 :ht"
  },
  {
    "path": "src/views/assembly/tabs/detail.vue",
    "chars": 560,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 我是 Tab 详情页 🍓🍇🍈🍉</span>\n    <span class=\"text\">params"
  },
  {
    "path": "src/views/assembly/tabs/index.vue",
    "chars": 3488,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 标签页操作 🍓🍇🍈🍉</span>\n    <div class=\"mb30\">\n      <el-i"
  },
  {
    "path": "src/views/assembly/treeFilter/index.scss",
    "chars": 213,
    "preview": ".content-box {\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  .descriptions-box {\n    display: fle"
  },
  {
    "path": "src/views/assembly/treeFilter/index.vue",
    "chars": 1944,
    "preview": "<template>\n  <div class=\"content-box\">\n    <TreeFilter\n      label=\"name\"\n      title=\"部门列表(单选)\"\n      :request-api=\"get"
  },
  {
    "path": "src/views/assembly/uploadFile/index.scss",
    "chars": 552,
    "preview": ".upload {\n  height: auto;\n  .card {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 1"
  },
  {
    "path": "src/views/assembly/uploadFile/index.vue",
    "chars": 8256,
    "preview": "<template>\n  <div class=\"upload content-box\">\n    <!-- 多图上传 -->\n    <div class=\"card img-box\">\n      <span class=\"text\">"
  },
  {
    "path": "src/views/assembly/wangEditor/index.scss",
    "chars": 109,
    "preview": ".el-button {\n  margin-top: 20px;\n}\n:deep(.el-dialog__body) {\n  height: 700px !important;\n  overflow: auto;\n}\n"
  },
  {
    "path": "src/views/assembly/wangEditor/index.vue",
    "chars": 1674,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\">富文本编辑器 🍓🍇🍈🍉</span>\n    <WangEditor v-model:value=\"con"
  },
  {
    "path": "src/views/auth/button/index.scss",
    "chars": 98,
    "preview": ".content-box {\n  align-items: flex-start;\n  span {\n    width: 100%;\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "src/views/auth/button/index.vue",
    "chars": 2374,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 按钮权限 🍓🍇🍈🍉</span>\n    <el-alert\n      class=\"mb20\"\n  "
  },
  {
    "path": "src/views/auth/menu/index.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/views/auth/menu/index.vue",
    "chars": 819,
    "preview": "<template>\n  <div class=\"card content-box\">\n    <span class=\"text\"> 菜单权限 🍓🍇🍈🍉</span>\n    <el-alert\n      :title=\"'目前菜单权限"
  },
  {
    "path": "src/views/dashboard/dataVisualize/components/curve.vue",
    "chars": 3471,
    "preview": "<template>\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\" name=\""
  },
  {
    "path": "src/views/dashboard/dataVisualize/components/pie.vue",
    "chars": 2643,
    "preview": "<template>\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\" name=\""
  },
  {
    "path": "src/views/dashboard/dataVisualize/index.scss",
    "chars": 3819,
    "preview": ".dataVisualize-box {\n  .top-box {\n    box-sizing: border-box;\n    padding: 25px 40px 0;\n    margin-bottom: 10px;\n    .to"
  },
  {
    "path": "src/views/dashboard/dataVisualize/index.vue",
    "chars": 3391,
    "preview": "<template>\n  <div class=\"dataVisualize-box\">\n    <div class=\"card top-box\">\n      <div class=\"top-title\">数据可视化</div>\n   "
  },
  {
    "path": "src/views/dataScreen/assets/alarmList.Json",
    "chars": 1009,
    "preview": "[\n  {\n    \"id\": 1,\n    \"warnMsg\": \"2022-04-25 14:09-23:09 预约人数 1,006 人次,已达到最大承载量的 99 %\",\n    \"label\": \"峨眉山\"\n  },\n  {\n   "
  },
  {
    "path": "src/views/dataScreen/assets/china.json",
    "chars": 1004439,
    "preview": "{\n  \"type\": \"FeatureCollection\",\n  \"features\": [\n    {\n      \"type\": \"Feature\",\n      \"properties\": {\n        \"adcode\": "
  },
  {
    "path": "src/views/dataScreen/assets/ranking-icon.ts",
    "chars": 82087,
    "preview": "/* 红色标注 */\nexport const ranking1: string =\n  \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAVCAYAAAAXQf3LAAAACXBI"
  },
  {
    "path": "src/views/dataScreen/components/AgeRatioChart.vue",
    "chars": 2908,
    "preview": "<template>\n  <!-- 年龄比例 -->\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" :resize=\"false\" />\n  </div>\n</template>"
  },
  {
    "path": "src/views/dataScreen/components/AnnualUseChart.vue",
    "chars": 5363,
    "preview": "<template>\n  <!-- 年度使用 -->\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" :resize=\"false\" />\n  </div>\n</template>"
  },
  {
    "path": "src/views/dataScreen/components/ChinaMapChart.vue",
    "chars": 4134,
    "preview": "<template>\n  <!-- 中国地图 -->\n  <div class=\"map-ball\"></div>\n  <div id=\"mapChart\" class=\"echarts\">\n    <ECharts :option=\"op"
  },
  {
    "path": "src/views/dataScreen/components/HotPlateChart.vue",
    "chars": 5195,
    "preview": "<template>\n  <!-- 热门板块 -->\n  <div class=\"echarts-header\">\n    <span>排名</span>\n    <span>景区</span>\n    <span>预约数量</span>\n"
  },
  {
    "path": "src/views/dataScreen/components/MaleFemaleRatioChart.vue",
    "chars": 3295,
    "preview": "<template>\n  <!-- 男女比例 -->\n  <div class=\"ratio-main\">\n    <div class=\"ratio-header\">\n      <div class=\"man\">\n        <sp"
  },
  {
    "path": "src/views/dataScreen/components/OverNext30Chart.vue",
    "chars": 4225,
    "preview": "<template>\n  <!-- 未来30天访问量趋势预测图 -->\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" :resize=\"false\" />\n  </div>\n</"
  },
  {
    "path": "src/views/dataScreen/components/PlatformSourceChart.vue",
    "chars": 6338,
    "preview": "<template>\n  <!-- 平台来源 -->\n  <div class=\"echarts\">\n    <ECharts :option=\"option\" :resize=\"false\" />\n  </div>\n</template>"
  },
  {
    "path": "src/views/dataScreen/components/RealTimeAccessChart.vue",
    "chars": 4501,
    "preview": "<template>\n  <!-- 实时访问 -->\n  <div class=\"actual-total\">\n    <div class=\"expect-total\">可预约总量<i>999999</i>人</div>\n    <div"
  },
  {
    "path": "src/views/dataScreen/index.scss",
    "chars": 7435,
    "preview": ".dataScreen-container {\n  width: 100%;\n  height: 100%;\n  background: url(\"./images/bg.png\") no-repeat;\n  background-repe"
  },
  {
    "path": "src/views/dataScreen/index.vue",
    "chars": 5628,
    "preview": "<template>\n  <div class=\"dataScreen-container\">\n    <div class=\"dataScreen-content\" ref=\"dataScreenRef\">\n      <div clas"
  },
  {
    "path": "src/views/directives/copyDirect/index.scss",
    "chars": 0,
    "preview": ""
  }
]

// ... and 78 more files (download for full content)

About this extraction

This page contains the full source code of the HalseySpicy/Geeker-Admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 278 files (1.5 MB), approximately 515.1k tokens, and a symbol index with 155 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!