master
刘鸿 2 years ago
commit 1deb1ea0bf
  1. 4
      .env
  2. 2
      .env.development
  3. 1
      .env.production
  4. 16
      .eslintignore
  5. 78
      .eslintrc.js
  6. 31
      .gitignore
  7. 6
      .husky/commit-msg
  8. 9
      .husky/common.sh
  9. 7
      .husky/lintstagedrc.js
  10. 8
      .husky/pre-commit
  11. 8
      .npmrc
  12. 18
      .prettierignore
  13. 3
      .vscode/extensions.json
  14. 35
      CHANGELOG.md
  15. 21
      LICENSE
  16. 245
      README.md
  17. 45
      commitlint.config.js
  18. 33
      config/constant.ts
  19. 30
      config/themeConfig.ts
  20. 27
      config/vite/optimizer.ts
  21. 11
      config/vite/plugin/autoImport.ts
  22. 41
      config/vite/plugin/component.ts
  23. 17
      config/vite/plugin/compress.ts
  24. 44
      config/vite/plugin/index.ts
  25. 23
      config/vite/plugin/mock.ts
  26. 78
      config/vite/plugin/styleImport.ts
  27. 17
      config/vite/plugin/svgIcons.ts
  28. 17
      config/vite/plugin/visualizer.ts
  29. 30
      config/vite/proxy.ts
  30. 13
      index.html
  31. 18
      mock/_createProdMockServer.ts
  32. 60
      mock/_util.ts
  33. 32
      mock/common.ts
  34. 89
      mock/home.ts
  35. 119
      mock/table.ts
  36. 98
      mock/user.ts
  37. 99
      package.json
  38. 9354
      pnpm-lock.yaml
  39. 5
      postcss.config.js
  40. 10
      prettier.config.js
  41. BIN
      public/favicon.ico
  42. 26
      src/App.vue
  43. 13
      src/api/common/index.ts
  44. 14
      src/api/common/model.d.ts
  45. 6
      src/api/global.d.ts
  46. 13
      src/api/home/index.ts
  47. 32
      src/api/home/model.d.ts
  48. 18
      src/api/sys/account/index.ts
  49. 16
      src/api/sys/account/model.d.ts
  50. 13
      src/api/user/index.ts
  51. 16
      src/api/user/model.d.ts
  52. BIN
      src/assets/images/Icon _Search.png
  53. BIN
      src/assets/images/Icon_Block.png
  54. BIN
      src/assets/images/Icon_contract.png
  55. BIN
      src/assets/images/Icon_node.png
  56. BIN
      src/assets/images/Icon_trading.png
  57. BIN
      src/assets/images/avatar.png
  58. BIN
      src/assets/images/login_bg.png
  59. BIN
      src/assets/images/logo.png
  60. 20
      src/components/Alert/index.vue
  61. 46
      src/components/Breadcrumb/index.vue
  62. 54
      src/components/Icon/index.vue
  63. 49
      src/components/Modal/index.vue
  64. 220
      src/components/Table/index.vue
  65. 132
      src/components/TableFilter/index.vue
  66. 96
      src/components/Upload/index.vue
  67. 11
      src/directives/index.ts
  68. 32
      src/directives/permission.ts
  69. 51
      src/directives/role.ts
  70. 27
      src/enums/authEnum.ts
  71. 38
      src/hooks/useBreadcrumbTitle.ts
  72. 108
      src/hooks/useECharts.ts
  73. 58
      src/hooks/useEventListener.ts
  74. 125
      src/hooks/useMessage.tsx
  75. 35
      src/hooks/usePermission.ts
  76. 27
      src/hooks/useRole.ts
  77. 45
      src/hooks/useTimeout.ts
  78. 23
      src/hooks/useTitle.ts
  79. 50
      src/layouts/BasicLayout/components/Header.vue
  80. 88
      src/layouts/BasicLayout/components/RightContent.vue
  81. 125
      src/layouts/BasicLayout/components/SideMenu.tsx
  82. 31
      src/layouts/BasicLayout/components/constant.ts
  83. 30
      src/layouts/BasicLayout/components/index.less
  84. 50
      src/layouts/BasicLayout/components/videoBox.vue
  85. 31
      src/layouts/BasicLayout/index.vue
  86. 47
      src/layouts/BasicLayout/utils/index.ts
  87. 59
      src/layouts/BasicLayout/utils/typings.ts
  88. 3
      src/layouts/BlankLayout.vue
  89. 30
      src/main.ts
  90. 62
      src/plugin.ts
  91. 12
      src/router/index.ts
  92. 49
      src/router/permission.ts
  93. 116
      src/router/router.config.ts
  94. 5
      src/shims-vue.d.ts
  95. 9
      src/store/index.ts
  96. 45
      src/store/modules/home.ts
  97. 134
      src/store/modules/permission.ts
  98. 62
      src/store/modules/sysAccount.ts
  99. 69
      src/store/modules/user.ts
  100. 5
      src/styles/antd.less
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,4 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api/hto-mp/v1
PORT=8998

@ -0,0 +1,2 @@
VITE_APP_ENV = dev
VITE_APP_TITLE = 我是标题

@ -0,0 +1 @@
VITE_APP_ENV = production

@ -0,0 +1,16 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile
.npmrc

@ -0,0 +1,78 @@
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true,
env: {
browser: true,
node: true,
es6: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:jest/recommended',
],
rules: {
'vue/script-setup-uses-vars': 'error',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
},
});

31
.gitignore vendored

@ -0,0 +1,31 @@
node_modules
.DS_Store
dist
.cache
package-lock.json
yarn.lock
.local
# local env files
.env.local
.env.*.local
.eslintcache
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
**/src/auto-imports.d.ts
**/src/components.d.ts

@ -0,0 +1,6 @@
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

@ -0,0 +1,9 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

@ -0,0 +1,7 @@
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'],
'*.{scss,less,styl,html}': ['prettier --write'],
};

@ -0,0 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ -n "$CI" ] && exit 0
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged

@ -0,0 +1,8 @@
# 提升一些依赖包至 node_modules
# 解决部分包模块not found的问题
# 用于配合 pnpm
#shamefully-hoist = true
# node-sass 下载问题
registry = "https://registry.npm.taobao.org"
sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"

@ -0,0 +1,18 @@
**/*.svg
**/*.ico
package.json
/dist
.DS_Store
.eslintignore
*.png
*.toml
.editorconfig
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
/public
**/node_modules/**
.npmrc

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

@ -0,0 +1,35 @@
# [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09)
# [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09)
# [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09)
# [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09)
# [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09)
### Bug Fixes
- nav ([a66af47](https://github.com/js-banana/vite-vue3-ts/commit/a66af4704c00fac48fcd6d30f1fa05f3c883aad9))
- **router:** 域名二级目录的路由配置优化 ([9b8c887](https://github.com/js-banana/vite-vue3-ts/commit/9b8c8876d61ebbdda7b16cc10aa19083517eceb2))
- **SideMenu:** 修复菜单文本与图标居中 ([60bafa0](https://github.com/js-banana/vite-vue3-ts/commit/60bafa0711b2f44df76f2979399ac95998576d67))
### Features
- **.env:** 增加环境变量配置文件 ([403029c](https://github.com/js-banana/vite-vue3-ts/commit/403029cb0ad703f4ea464a81876987a64b570f37))
- 调整权限逻辑,补充 v-role 指令 ([9a9598b](https://github.com/js-banana/vite-vue3-ts/commit/9a9598b2bb85a5c8baf5a08c56efd0e308514b96))
- 添加路由动效,抽离 Breadcrumb 组件 ([d32087c](https://github.com/js-banana/vite-vue3-ts/commit/d32087c9f9490f6245589fce42342b19e3068b5e))
- 增加 Table 使用 demo,完善文档说明,优化 Table API,保持与官方 antv 一致 ([159e0da](https://github.com/js-banana/vite-vue3-ts/commit/159e0da34c2897d4d2a3433ac793cfa3c4521cf2))
- vite2.x => vite3.x 工具链生态相关升级更新 ([820c02e](https://github.com/js-banana/vite-vue3-ts/commit/820c02e0f1eec256bf4fc9884cb25f2631fa803e))
### Performance Improvements
- 路由模式由 hash 调整为 history ([e37f2f6](https://github.com/js-banana/vite-vue3-ts/commit/e37f2f60291241ec4ab30134afd46b0dd83815a8))
## [0.0.1](https://github.com/js-banana/vite-vue3-ts/compare/219fe493bd2623c0abfab0e4ef48a2a12838ccdf...v0.0.1) (2021-12-13)
### Features
- **app:** 生产环境 mock、功能组件、路由完善 ([74b1983](https://github.com/js-banana/vite-vue3-ts/commit/74b1983c7a946b2fb1a95afcd3870a13db96fa9b))
- **mock:** mock 数据编写 ([219fe49](https://github.com/js-banana/vite-vue3-ts/commit/219fe493bd2623c0abfab0e4ef48a2a12838ccdf))
- update app and docs ([b61d9ce](https://github.com/js-banana/vite-vue3-ts/commit/b61d9cea26c522850eca74cb0833d5efd90c52c1))

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, JS-banana
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.

@ -0,0 +1,245 @@
# vite-vue3-ts
[![ci](https://github.com/JS-banana/vite-vue3-ts/actions/workflows/deploy.yml/badge.svg)](https://github.com/JS-banana/vite-vue3-ts/actions/workflows/deploy.yml)
## 介绍
一个使用 `vite` + `vue3` + `pinia` + `ant-design-vue` + `typescript` 完整技术路线开发的项目,秒级开发更新启动、新的`vue3 composition api` 结合 `setup`纵享丝滑般的开发体验、全新的 `pinia`状态管理器和优秀的设计体验(`1k`的size)、`antd`无障碍过渡使用UI组件库 `ant-design-vue`、安全高效的 `typescript`类型支持、代码规范验证、多级别的权限管理~
相关文章:<https://juejin.cn/post/7041188884864040991>
本项目相关改动及更新见【[更新记录](#更新记录)↓↓↓】
## 特性
- ✨脚手架工具:高效、快速的 **Vite**
- 🔥前端框架:眼下最时髦的 **Vue3**
- 🍍状态管理器:`vue3`新秀 **Pinia**,犹如 `react zustand`般的体验,友好的api和异步处理
- 🏆开发语言:政治正确 **TypeScript**
- 🎉UI组件:`antd`开发者无障碍过渡使用 **ant-design-vue**,熟悉的配方熟悉的味道
- 🎨css样式:**less** 、`postcss`
- 📖代码规范:**Eslint**、**Prettier**、**Commitlint**
- 🔒权限管理:页面级、菜单级、按钮级、接口级
- ✊依赖按需加载:**unplugin-auto-import**,可自动导入使用到的`vue`、`vue-router`等依赖
- 💪组件按需导入:**unplugin-vue-components**,无论是第三方UI组件还是自定义组件都可实现自动按需导入以及`TS`语法提示
## 项目目录
```js
├── .husky // husky git hooks配置目录
├── _ // husky 脚本生成的目录文件
├── commit-msg // commit-msg钩子,用于验证 message格式
├── pre-commit // pre-commit钩子,主要是和eslint配合
├── config // 全局配置文件
├── vite // vite 相关配置
├── constant.ts // 项目配置
├── themeConfig.ts // 主题配置
├── dist // 默认的 build 输出目录
├── mock // 前端数据mock
├── public // vite项目下的静态目录
└── src // 源码目录
├── api // 接口相关
├── assets // 公共的文件(如image、css、font等)
├── components // 项目组件
├── directives // 自定义 指令
├── enums // 自定义 常量(枚举写法)
├── hooks // 自定义 hooks
├── layout // 全局布局
├── router // 路由
├── store // 状态管理器
├── utils // 工具库
├── views // 页面模块目录
├── login // login页面模块
├── ...
├── App.vue // vue顶层文件
├── auto-imports.d.ts // unplugin-auto-import 插件生成
├── components.d.d.ts // unplugin-vue-components 插件生成
├── main.ts // 项目入口文件
├── shimes-vue.d.ts // vite默认ts类型文件
├── types // 项目type类型定义文件夹
├── .editorconfig // IDE格式规范
├── .env // 环境变量
├── .eslintignore // eslint忽略
├── .eslintrc // eslint配置文件
├── .gitignore // git忽略
├── .npmrc // npm配置文件
├── .prettierignore // prettierc忽略
├── .prettierrc // prettierc配置文件
├── index.html // 入口文件
├── LICENSE.md // LICENSE
├── package.json // package
├── pnpm-lock.yaml // pnpm-lock
├── postcss.config.js // postcss
├── README.md // README
├── tsconfig.json // typescript配置文件
└── vite.config.ts // vite
```
## 使用说明
> 简要说明:
>
> 随着vite3.x的发布,本项目针对该依赖的相关生态做了升级,详情见分支 [feat-vite3.x](https://github.com/JS-banana/vite-vue3-ts/tree/feat-vite3.x)
>
> 需要指出的是vite3.x要求node14.18及以上,详情见 [从 v2 迁移](https://cn.vitejs.dev/guide/migration.html)
1. 克隆本项目
```sh
git clone https://github.com/JS-banana/vite-vue3-ts.git
```
2. 安装依赖
```sh
# 推荐使用 pnpm
pnpm install
# 没有安装的直接安装
npm install -g pnpm
```
3. 启动项目
```sh
pnpm serve
# or
pnpm dev
```
4. 部署
```sh
# 检查TS类型然后构建打包
pnpm build:check
# 跳过检查直接构建打包
pnpm build
# 预览
pnpm preview
```
### 数据模拟
为了实现更多元化和真实数据展示,使用了Mock+fakerjs进行数据模拟,fakerjs的功能极其强大,几乎可以定制任何类型数据,本项目里做了部分演示,源码见`mock/table.ts`
### ant-design-vue 2.x升级到3.x的说明
Table组件:
在2.x版本的时候,Table组件主要通过 `columns`属性,配置字段 `slots: { customRender: 'action' }`进行自定义插槽,做到制定内容的内容,基于此,本项目Table组件封装的内部实现为`<template v-if="column.slots?.customRender === 'action'">`。
但是在3.x之后,官方舍弃了 columns中的`slots`属性,因此该方式需要做出调整,不过,我们的整体思路不变,即,确定一个通用且唯一的key为判断属性即可,以实现对不同内容的区别渲染。
目前的方案打算以columns中的`key` prop属性作为唯一判断的key,Table组件封装内部实现如`<template v-if="column.key === 'action'">`,因此,在columns中默认以`dataIndex`作为渲染key,而`key`作为我们的自定义渲染插槽内容的key。
官网API介绍如下:
| Property | Description | Type | Default |
| --------- | ------------------------------------------------------------------------------------ | --------------- | ------- |
| dataIndex | Display field of the data record, support nest path by string array | string/string[] | - |
| key | Unique key of this column, you can ignore this prop if you've set a unique dataIndex | string | - |
### Table高级组件的基本用法
> 详情及源码见:./src/components/Table/index.vue
几个核心API:
| Property | Description | Type | Default |
| ---------------- | ------------------------------------------------------------------------------------------- | -------------------- | ------- |
| url | 接受一个异步请求函数,Table内部会自动接管状态(包括发起请求、分页参数等) | Promise函数 | - |
| columns | 配置列表项 | 与官方API一致 | - |
| hiddenFilter | 是否展示Table上面的筛选项组件 | Boolean | - |
| tableActions | 自定义action slot(需要在columns中指定`key: 'action'`) | ref/reactive类型数组 | - |
| button | 筛选组件中的独立按钮(如:新增用户) | ref/reactive类型对象 | - |
| items | 筛选组件中的表单项(如:选择角色、搜索名称,会自动生成查询按钮,并接管到Table中的参数状态) | ref/reactive类型数组 | - |
| tableFilterModel | 筛选组件中的表单项数据模型(配合items使用,为了确定key) | ref/reactive类型对象 | - |
```html
<template>
<Table
ref="ELRef"
:url="fetchApi.list"
:columns="columns"
:actions="tableActions"
:button="tableFilterButton"
:items="tableFilterItems"
:model="tableFilterModel"
/>
</template>
```
使用ref调用Table提供的方法API:
| Property | Description | Type | Default |
| -------- | -------------------------------------------------------------------------------------------- | -------------- | ------- |
| refresh | 不接受参数,使用当前的参数发起更新请求(在新增或删除、修改一条数据后,调用该方法进行列表更新 | ()=>void | - |
| run | 接受参数,当需要与自定义参数合并时,并发起新的请求,使用此方 | (params)=>void | - |
```js
const refresh = () => ELRef.value?.refresh();
// const run = (args) => ELRef.value.run(args);
```
### 特别说明
`config/vite/plugin/mock.ts` 中的 `configMockPlugin` 方法,属性`prodEnabled` 在生产环境一定要关闭,不然会把大量的mock代码,如fakerjs中的一些源码打包进构建包内。
**本项目这里为了做演示,是手动开启了的,为了能在线上部署的时候查看mock数据,实际开发中一定注意关闭!!!**
## 效果图
![vite-vue3-3](https://cdn.jsdelivr.net/gh/JS-banana/images/vuepress/vite-vue3-3.jpg)
![vite-vue3-4](https://cdn.jsdelivr.net/gh/JS-banana/images/vuepress/vite-vue3-4.jpg)
## 更新记录
- 2022.01.18
- 增加环境变量配置文件 `.env`/`.env.development`/`.env.production`
- 2022.03.09
- 为了优化服务器构建,移除 `auto-imports.d.ts`、`components.d.ts`的git记录,加入`.gitignore`
- 域名二级目录的路由配置优化 `history: createWebHistory(import.meta.env.BASE_URL)`
- 路由模式由 hash调整为 history
- 2022.05.07
- 添加路由动效`transition`,优化用户体验,并抽离封装`Breadcrumb`组件
- 添加权限指令`v-role`,调整权限逻辑,目前权限指令包括`v-role`/`v-auth`
- `Table`相关组件有所改动,同步迭代了一些功能点,包括优化项
- 建议配合该篇文章食用[多级别权限设计思考及实战](https://ssscode.com/pages/ff7971/)
- 2022.06.21
- `ant-design-vue`升级到`3.x`版本,同步更新改动了一些API
- `dayjs`替换`moment`
- 2022.07.24
- ✔完善`Table`组件,更新了一些在项目中迭代的优化
- ✔优化`Table`相关`API`,遵循`ant-design-vue3.x`官方用法进行迭代
- ✔新增`Table`使用demo,增加各API用法示例,基本涵盖大部分用法
- ✔新增`fakerjs`数据mock,配合`Mockjs`完善并增强对不同数据类型和场景的模拟
- ✔完善文档,新增使用说明
- [PR](https://github.com/JS-banana/vite-vue3-ts/pull/6)
- 2022.07.30
- vite相关工具链升级到3.x
- 现在你必须使用 Node 14.18+ / 16+ 版本。
- 详情见分支 [feat-vite3.x](https://github.com/JS-banana/vite-vue3-ts/tree/feat-vite3.x)
- 原有的vite2.x版本见分支 [feat-vite2.x](https://github.com/JS-banana/vite-vue3-ts/tree/feat-vite2.x)
- 现在master主分支为最新的vite3.x版本
## 计划
- [x] `ant-design-vue` 升级到 3.x版本
- [ ] 主题换肤功能
- [ ] 引入 `tailwindcss`
## 交流
你可以关注我公众号(前端小帅),加我微信交流,一起沟通学习~
<table>
<tr>
<td valign="top">
<img height="160" alt="公众号:前端小帅" src="https://cdn.jsdelivr.net/gh/JS-banana/images/vuepress/4.png" />
</td>
</tr>
</table>
## 感谢star
[![Stargazers over time](https://starchart.cc/JS-banana/vite-vue3-ts.svg)](https://starchart.cc/JS-banana/vite-vue3-ts)

@ -0,0 +1,45 @@
// feat:新增功能
// fix:bug 修复
// docs:文档更新
// style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
// refactor:重构代码(既没有新增功能,也没有修复 bug)
// perf:性能, 体验优化
// test:新增测试用例或是更新现有测试
// build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
// ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
// chore:不属于以上类型的其他类,比如构建流程, 依赖管理
// revert:回滚某个更早之前的提交
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
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',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
};

@ -0,0 +1,33 @@
/**
* @name Config
* @description
*/
// 应用名
export const APP_TITLE = 'Vite-Vue3-Admin';
// 本地服务端口
export const VITE_PORT = 3000;
// prefix
export const API_PREFIX = '/api';
// serve
export const API_BASE_URL = '/api';
export const API_TARGET_URL = 'http://localhost:8080';
// mock
export const MOCK_API_BASE_URL = '/mock/api';
export const MOCK_API_TARGET_URL = 'http://localhost:3000';
// iconfontUrl
export const ICONFONTURL = '//at.alicdn.com/t/font_3004192_9jmc1z9neiw.js'; // 去色版
// 包依赖分析
export const ANALYSIS = true;
// 代码压缩
export const COMPRESSION = true;
// 删除 console
export const VITE_DROP_CONSOLE = true;

@ -0,0 +1,30 @@
import { getThemeVariables } from 'ant-design-vue/dist/theme';
// @primary-color: #1890ff; // 全局主色
// @link-color: #1890ff; // 链接色
// @success-color: #52c41a; // 成功色
// @warning-color: #faad14; // 警告色
// @error-color: #f5222d; // 错误色
// @font-size-base: 14px; // 主字号
// @heading-color: rgba(0, 0, 0, 0.85); // 标题色
// @text-color: rgba(0, 0, 0, 0.65); // 主文本色
// @text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
// @disabled-color: rgba(0, 0, 0, 0.25); // 失效色
// @border-radius-base: 4px; // 组件/浮层圆角
// @border-color-base: #d9d9d9; // 边框色
// @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影
/**
* less global variable
*/
export function generateModifyVars(dark = false) {
const modifyVars = getThemeVariables({ dark });
return {
...modifyVars,
// Used for global import to avoid the need to import each style file separately
// reference: Avoid repeated references
// hack: `${modifyVars.hack} @import (reference) "${resolve('src/design/config.less')}";`,
'primary-color': '#3860F4',
'link-color': '#3860F4',
};
}

@ -0,0 +1,27 @@
/**
* @name configManualChunk
* @description chunk
*/
const vendorLibs: { match: string[]; output: string }[] = [
{
match: ['ant-design-vue'],
output: 'antdv',
},
{
match: ['echarts'],
output: 'echarts',
},
];
// pnpm安装的依赖,获取到的路径名称是拼接而成且比较长的
// vite-vue3-ts/node_modules/.pnpm/registry.npmmirror.com+ant-design-vue@3.2.7_vue@3.2.23/node_modules/ant-design-vue/es/card/style/index.js
export const configManualChunk = (id: string) => {
if (/[\\/]node_modules[\\/]/.test(id)) {
const matchItem = vendorLibs.find((item) => {
const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
return reg.test(id);
});
return matchItem ? matchItem.output : null;
}
};

@ -0,0 +1,11 @@
/**
* @name AutoImportDeps
* @description
*/
import AutoImport from 'unplugin-auto-import/vite';
export const AutoImportDeps = () =>
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
});

@ -0,0 +1,41 @@
/**
* @name autoRegistryComponents
* @description
*/
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
export const autoRegistryComponents = () => {
return Components({
// relative paths to the directory to search for components.
dirs: ['src/components'],
// valid file extensions for components.
extensions: ['vue'],
// search for subdirectories
deep: true,
// resolvers for custom components
resolvers: [AntDesignVueResolver({ importStyle: 'less' })],
// generate `components.d.ts` global declarations,
// also accepts a path for custom filename
// dts: false,
dts: 'src/components.d.ts',
// Allow subdirectories as namespace prefix for components.
directoryAsNamespace: false,
// Subdirectory paths for ignoring namespace prefixes
// works when `directoryAsNamespace: true`
globalNamespaces: [],
// auto import for directives
// default: `true` for Vue 3, `false` for Vue 2
// Babel is needed to do the transformation for Vue 2, it's disabled by default for performance concerns.
// To install Babel, run: `npm install -D @babel/parser @babel/traverse`
directives: true,
// filters for transforming targets
include: [/\.vue$/, /\.vue\?vue/],
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
});
};

@ -0,0 +1,17 @@
/**
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import compressPlugin from 'vite-plugin-compression';
import { COMPRESSION } from '../../constant';
export function configCompressPlugin(): Plugin | Plugin[] {
if (COMPRESSION) {
return compressPlugin({
ext: '.gz',
deleteOriginFile: false,
}) as Plugin;
}
return [];
}

@ -0,0 +1,44 @@
import type { Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
// import legacy from '@vitejs/plugin-legacy';
import { configStyleImportPlugin } from './styleImport';
import { configSvgIconsPlugin } from './svgIcons';
import { autoRegistryComponents } from './component';
import { AutoImportDeps } from './autoImport';
import { configMockPlugin } from './mock';
import { configVisualizerConfig } from './visualizer';
import { configCompressPlugin } from './compress';
export function createVitePlugins(isBuild: boolean) {
const vitePlugins: (Plugin | Plugin[])[] = [
// vue支持
vue(),
// JSX支持
vueJsx(),
// 自动按需引入组件
autoRegistryComponents(),
// 自动按需引入依赖
AutoImportDeps(),
];
// @vitejs/plugin-legacy
// isBuild && vitePlugins.push(legacy());
// rollup-plugin-gzip
isBuild && vitePlugins.push(configCompressPlugin());
// vite-plugin-svg-icons
vitePlugins.push(configSvgIconsPlugin(isBuild));
// vite-plugin-mock
vitePlugins.push(configMockPlugin(isBuild));
// rollup-plugin-visualizer
vitePlugins.push(configVisualizerConfig());
// vite-plugin-style-import
vitePlugins.push(configStyleImportPlugin(isBuild));
return vitePlugins;
}

@ -0,0 +1,23 @@
/**
* Mock plugin for development and production.
* https://github.com/anncwb/vite-plugin-mock
*/
import { viteMockServe } from 'vite-plugin-mock';
export function configMockPlugin(isBuild: boolean) {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild, // 为了演示,线上开启 mock,实际开发请关闭,会影响打包体积
// 开发环境无需关心
// injectCode 只受prodEnabled影响
// https://github.com/anncwb/vite-plugin-mock/issues/9
// 下面这段代码会被注入 main.ts
injectCode: `
import { setupProdMockServer } from '../mock/_createProdMockServer';
setupProdMockServer();
`,
});
}

@ -0,0 +1,78 @@
/**
* Introduces component library styles on demand.
* https://github.com/anncwb/vite-plugin-style-import
*/
import styleImport from 'vite-plugin-style-import';
export function configStyleImportPlugin(isBuild: boolean) {
if (!isBuild) {
return [];
}
const styleImportPlugin = styleImport({
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
// 这里是无需额外引入样式文件的“子组件”列表
const ignoreList = [
'anchor-link',
'sub-menu',
'menu-item',
'menu-item-group',
'breadcrumb-item',
'breadcrumb-separator',
'form-item',
'step',
'select-option',
'select-opt-group',
'card-grid',
'card-meta',
'collapse-panel',
'descriptions-item',
'list-item',
'list-item-meta',
'table-column',
'table-column-group',
'tab-pane',
'tab-content',
'timeline-item',
'tree-node',
'skeleton-input',
'skeleton-avatar',
'skeleton-title',
'skeleton-paragraph',
'skeleton-image',
'skeleton-button',
];
// 这里是需要额外引入样式的子组件列表
// 单独引入子组件时需引入组件样式,否则会在打包后导致子组件样式丢失
const replaceList = {
'typography-text': 'typography',
'typography-title': 'typography',
'typography-paragraph': 'typography',
'typography-link': 'typography',
'dropdown-button': 'dropdown',
'input-password': 'input',
'input-search': 'input',
'input-group': 'input',
'radio-group': 'radio',
'checkbox-group': 'checkbox',
'layout-sider': 'layout',
'layout-content': 'layout',
'layout-footer': 'layout',
'layout-header': 'layout',
'month-picker': 'date-picker',
};
return ignoreList.includes(name)
? ''
: replaceList.hasOwnProperty(name)
? `ant-design-vue/es/${replaceList[name]}/style/index`
: `ant-design-vue/es/${name}/style/index`;
},
},
],
});
return styleImportPlugin;
}

@ -0,0 +1,17 @@
/**
* Vite Plugin for fast creating SVG sprites.
* https://github.com/anncwb/vite-plugin-svg-icons
*/
import SvgIconsPlugin from 'vite-plugin-svg-icons';
import path from 'path';
export function configSvgIconsPlugin(isBuild: boolean) {
const svgIconsPlugin = SvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
svgoOptions: isBuild,
// default
symbolId: 'icon-[dir]-[name]',
});
return svgIconsPlugin;
}

@ -0,0 +1,17 @@
/**
* Package file volume analysis
*/
import visualizer from 'rollup-plugin-visualizer';
import { ANALYSIS } from '../../constant';
export function configVisualizerConfig() {
if (ANALYSIS) {
return visualizer({
filename: './node_modules/.cache/visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}) as Plugin;
}
return [];
}

@ -0,0 +1,30 @@
/**
* Generate proxy
*/
import {
API_BASE_URL,
API_TARGET_URL,
MOCK_API_BASE_URL,
MOCK_API_TARGET_URL,
} from '../../config/constant';
import { ProxyOptions } from 'vite';
type ProxyTargetList = Record<string, ProxyOptions>;
const ret: ProxyTargetList = {
// test
[API_BASE_URL]: {
target: API_TARGET_URL,
changeOrigin: true,
//rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''),
},
// mock
[MOCK_API_BASE_URL]: {
target: MOCK_API_TARGET_URL,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'),
},
};
export default ret;

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>liuh's tools</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

@ -0,0 +1,18 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
const modules = import.meta.globEager('./**/*.ts');
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
mockModules.push(...modules[key].default);
});
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules);
}

@ -0,0 +1,60 @@
// Interface data format used to return a unified format
export function resultSuccess<T = Recordable>(result: T, { message = 'ok' } = {}) {
return {
code: 0,
result,
message,
type: 'success',
};
}
export function resultPageSuccess<T = any>(
page: number,
pageSize: number,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(page, pageSize, list);
return {
...resultSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
return {
code,
result,
message,
type: 'error',
};
}
export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
const offset = (pageNo - 1) * Number(pageSize);
const ret =
offset + Number(pageSize) >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + Number(pageSize));
return ret;
}
export interface requestParams {
method: string;
body: any;
headers?: { authorization?: string };
query: any;
}
/**
* @description request数据中获取token
*
*/
export function getRequestToken({ headers }: requestParams): string | undefined {
return headers?.authorization;
}

@ -0,0 +1,32 @@
import Mock from 'mockjs';
const list = Mock.mock({
'items|60': [
{
id: '@id',
url: '@url',
ip: '@ip',
protocol: '@protocol',
'host|1': [80, 443],
domain: '@domain',
email: '@email',
},
],
});
export default [
{
url: '/v1/common/page_one/list',
method: 'get',
response: () => {
const items = list.items;
return {
code: 0,
result: {
total: items.length,
list: items,
},
};
},
},
];

@ -0,0 +1,89 @@
import Mock from 'mockjs';
import { resultSuccess } from './_util';
const list = Mock.mock({
'items|30': [
{
id: '@id',
title: '@ctitle',
mobile: '@phone',
name: '@cname',
description: '@cparagraph',
created_at: '@datetime',
updated_at: '@datetime',
age: '@natural(10,50)',
color: '@color',
email: '@email',
},
],
});
const data = {
hu_num: 42,
yun_num: 87755,
ce_num: 3,
create_time: 1636352741,
online_num: 101,
total_num: 110,
seven_days: [
{
id: 9,
num: 7,
time: '20211130',
},
{
id: 8,
num: 80,
time: '20211129',
},
{
id: 0,
num: 280,
time: '20211128',
},
{
id: 0,
num: 0,
time: '20211127',
},
{
id: 7,
num: 5,
time: '20211126',
},
{
id: 6,
num: 20,
time: '20211125',
},
{
id: 5,
num: 5,
time: '20211124',
},
],
};
export default [
{
url: '/v1/home/info',
method: 'get',
response: () => {
return resultSuccess(data);
},
},
{
url: '/v1/home/list',
method: 'get',
response: () => {
const items = list.items;
return {
code: 0,
result: {
total: items.length,
list: items,
},
};
},
},
];

@ -0,0 +1,119 @@
import Mock from 'mockjs';
import { faker } from '@faker-js/faker';
import { getRequestToken, resultError } from './_util';
import { createFakeUserList } from './user';
enum URL {
table = '/table/list',
list = '/v1/node/nodelist',
}
const data = Mock.mock({
'items|30': [
{
id: '@id',
title: '@sentence(10, 20)',
account: '@phone',
true_name: '@name',
created_at: '@datetime',
role_name: '@name',
},
],
});
const NAMES = ['节点1', '节点2', '节点3', '节点4'];
const DATA_names = ['机构1', '机构2', '机构3'];
const DATA_blockList = Mock.mock({
'items|23': [
{
'id|+1': 1,
node_name: () => faker.helpers.arrayElement(NAMES),
institutions_name: () => faker.helpers.arrayElement(DATA_names),
ip: () => faker.internet.ip(),
port: () => faker.internet.port(),
nodeRole: () => faker.helpers.arrayElement(['普通节点', '管理节点']),
is_consensus: () => faker.helpers.arrayElement(['是', '否']),
create_time: () => faker.date.past(2, new Date().toISOString()),
status: () => faker.helpers.arrayElement(['正常', '异常']),
isSelf: () => faker.datatype.boolean(),
},
],
});
export default [
{
url: URL.table,
method: 'get',
response: () => {
const items = data.items;
return {
code: 0,
result: {
total: items.length,
list: items,
},
};
},
},
{
url: URL.list,
method: 'get',
response: (request) => {
let items = DATA_blockList.items.map((n) => {
return {
...n,
node_name: n.nodeRole === '创世节点' ? '创世节点' : n.node_name,
institutions_name:
n.nodeRole === '管理节点'
? '管理'
: ['节点1', '节点2'].includes(n.node_name)
? n.node_name.replace('节点', '机构')
: n.institutions_name,
};
});
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = createFakeUserList().find((item) => item.token === token);
if (checkUser?.role === 0) {
items = [
{
id: 14,
node_name: '节点1',
institutions_name: '机构1',
ip: '147.174.206.1',
port: 26042,
nodeRole: '普通节点',
is_consensus: '否',
create_time: '2021-02-25T02:27:18.151Z',
status: '正常',
isSelf: true,
isUpgrade: true,
},
{
id: 15,
node_name: '节点2',
institutions_name: '机构2',
ip: '147.174.6.190',
port: 26042,
nodeRole: '普通节点',
is_consensus: '是',
create_time: '2021-03-25T02:25:18.151Z',
status: '正常',
isSelf: false,
isUpgrade: false,
},
];
}
return {
code: 0,
result: {
total: items.length,
list: items,
},
};
},
},
];

@ -0,0 +1,98 @@
import { MockMethod } from 'vite-plugin-mock';
import { resultError, resultSuccess, getRequestToken, requestParams } from './_util';
export function createFakeUserList() {
return [
{
userId: '1',
username: 'admin',
realName: 'sssgoEasy Admin',
avatar: '',
desc: 'manager',
password: '123456',
token: 'fakeToken1',
auths: [],
modules: [],
is_admin: 1,
role_name: '管理员角色',
mobile: 13000000000,
last_login: '2021-11-11 12:00',
role: 1, // 管理
},
{
userId: '2',
username: 'test',
password: '123456',
realName: 'test user',
avatar: '',
desc: 'tester',
token: 'fakeToken2',
auths: [],
modules: ['home', 'website'],
is_admin: 0,
role_name: '普通用户角色',
mobile: 18000000000,
last_login: '2021-11-11 12:12',
role: 0, // 普通
},
];
}
export default [
// mock user login
{
url: '/v1/user/login',
timeout: 200,
method: 'post',
response: ({ body }) => {
const { username, password } = body;
const checkUser = createFakeUserList().find(
(item) => item.username === username && password === item.password,
);
if (!checkUser) {
return resultError('Incorrect account or password!');
}
return resultSuccess(checkUser);
},
},
{
url: '/v1/user/permission',
method: 'get',
response: (request: requestParams) => {
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = createFakeUserList().find((item) => item.token === token);
if (!checkUser) {
return resultError('The corresponding user information was not obtained!');
}
return resultSuccess(checkUser);
},
},
{
url: '/v1/user/logout',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = createFakeUserList().find((item) => item.token === token);
if (!checkUser) {
return resultError('Invalid token!');
}
return resultSuccess(undefined, { message: 'Token has been destroyed' });
},
},
{
url: '/v1/account/info',
method: 'get',
response: (request: requestParams) => {
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = createFakeUserList().find((item) => item.token === token);
if (!checkUser) {
return resultError('The corresponding user information was not obtained!');
}
return resultSuccess(checkUser);
},
},
] as MockMethod[];

@ -0,0 +1,99 @@
{
"name": "vite-vue3-ts",
"version": "1.0.0",
"license": "MIT",
"description": "a vite + vue3 + pinia + typescript + ant-design-vue template",
"keywords": [
"vite",
"vue3",
"vue-router",
"setup",
"typescript",
"pinia",
"ant-design-vue",
"template"
],
"homepage": "https://github.com/js-banana/vite-vue3-ts",
"repository": {
"type": "git",
"url": "git+https://github.com/js-banana/vite-vue3-ts.git"
},
"engines": {
"node": ">=14"
},
"scripts": {
"bootstrap": "pnpm install",
"check": "vue-tsc --noEmit",
"serve": "npm run dev",
"dev": "vite",
"dev:development": "vite --mode development",
"build:production": "vue-tsc --noEmit && vite build --mode production",
"build": "vite build",
"build:check": "vue-tsc --noEmit && vite build",
"build:github": "vite build --base=/vite-vue3-ts/",
"build:no-cache": "pnpm clean:cache && npm run build",
"preview": "vite preview",
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
"clean:lib": "rimraf node_modules",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && npx prettier CHANGELOG.md --write && git add CHANGELOG.md",
"prepare": "husky install"
},
"dependencies": {
"@ant-design/icons-vue": "^6.0.1",
"@vueuse/core": "^6.8.0",
"ant-design-vue": "^3.2.7",
"axios": "^0.24.0",
"dayjs": "^1.11.3",
"echarts": "^5.2.2",
"lodash-es": "^4.17.21",
"pinia": "^2.0.3",
"sanitize.css": "^13.0.0",
"vue": "^3.2.16",
"vue-request": "^1.2.3",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"@faker-js/faker": "^7.3.0",
"@types/lodash-es": "^4.17.5",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"@vitejs/plugin-legacy": "^2.0.0",
"@vitejs/plugin-vue": "^3.0.1",
"@vitejs/plugin-vue-jsx": "^2.0.0",
"autoprefixer": "^10.4.0",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-define-config": "~1.0.9",
"eslint-plugin-jest": "^25.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.17.0",
"husky": "^7.0.4",
"jest": "^27.4.2",
"less": "^4.1.2",
"lint-staged": "^11.2.6",
"mockjs": "^1.1.0",
"postcss": "^8.3.11",
"postcss-html": "^1.2.0",
"postcss-less": "^5.0.0",
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.7.1",
"terser": "^5.14.2",
"typescript": "^4.4.3",
"unplugin-auto-import": "^0.4.20",
"unplugin-vue-components": "^0.17.21",
"vite": "^3.0.4",
"vite-plugin-compression": "^0.3.6",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^1.4.0",
"vite-plugin-svg-icons": "^1.0.5",
"vue-eslint-parser": "^7.11.0",
"vue-tsc": "^0.3.0"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

@ -0,0 +1,10 @@
module.exports = {
printWidth: 100,
semi: true,
vueIndentScriptAndStyle: true,
singleQuote: true,
trailingComma: 'all',
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,26 @@
<template>
<ConfigProvider :locale="zhCN">
<router-view />
</ConfigProvider>
</template>
<script lang="ts" setup>
import { ConfigProvider } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import { useTitle } from '/@/hooks/useTitle';
// date-picker
// distimport 'moment/dist/locale/zh-cn'
// moment
// import moment from 'moment';
// import 'moment/dist/locale/zh-cn';
// moment.locale('zh_CN');
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
useTitle();
console.log('my config env: ', import.meta.env);
</script>

@ -0,0 +1,13 @@
import { ReqParams, ResResult } from './model';
import { get } from '/@/utils/http';
enum URL {
page_one_list = '/v1/common/page_one/list',
list = '/v1/node/nodelist',
}
const page_one_list = async (data: ReqParams) => get<ResResult>({ url: URL.page_one_list, data });
const node_list = async (data: ReqParams) => get<ResResult>({ url: URL.list, data });
export default { page_one_list, node_list };

@ -0,0 +1,14 @@
export interface ReqParams {
limit: number;
page: number;
}
export interface ResResult {
id: number;
url: string;
ip: string;
protocol: string;
host: number;
domain: string;
email: string;
}

@ -0,0 +1,6 @@
// 接口返回 形状
export interface ResData<T> {
code: number;
message: string;
result: T;
}

@ -0,0 +1,13 @@
import { ReqParams, ResInfoList, ResResult } from './model';
import { get } from '/@/utils/http';
enum URL {
list = '/v1/home/list',
info = '/v1/home/info',
}
const list = async (data: ReqParams) => get<ResResult>({ url: URL.list, data });
const info = async () => get<ResInfoList>({ url: URL.info });
export default { list, info };

@ -0,0 +1,32 @@
export interface ReqParams {
limit: number;
page: number;
}
export interface ResResult {
id: string;
title: string;
name: string;
description: string;
created_at: string;
updated_at: string;
age: number;
color: string;
email: string;
}
interface ResInfoListItem {
id: number;
num: number;
time: string;
}
export interface ResInfoList {
hu_num: number;
yun_num: number;
ce_num: number;
create_time: number;
online_num: number;
total_num: number;
seven_days: ResInfoListItem[];
}

@ -0,0 +1,18 @@
/**
* @name account
* @description -
*/
import { ReqAccount, ResAccount } from './model';
import { get, post } from '/@/utils/http';
enum URL {
update = '/v1/account/edit',
account = '/v1/account/info',
}
const account = async () => get<ResAccount>({ url: URL.account });
const update = async (data: ReqAccount) => post<any>({ url: URL.update, data });
export default { account, update };

@ -0,0 +1,16 @@
export interface ReqAccount {
id: number;
account?: string;
password?: string;
}
export interface ResAccount {
account: string;
last_login: string;
mobile: string;
role_name: string;
true_name: string;
user_id: number;
}
export type ResPermission = { auths: Array<string> };

@ -0,0 +1,13 @@
import { ReqAuth, ReqParams, ResResult } from './model';
import { get, post } from '/@/utils/http';
enum URL {
login = '/v1/user/login',
permission = '/v1/user/permission',
}
const login = async (data: ReqParams) => post<ResResult>({ url: URL.login, data });
const permission = async () => get<ReqAuth>({ url: URL.permission });
export default { login, permission };

@ -0,0 +1,16 @@
export interface ReqParams {
mobile: 'string';
password: 'string';
}
export interface ReqAuth {
auths: string[];
modules: string[];
is_admin?: 0 | 1;
}
export interface ResResult {
login_status: number;
st: string;
token: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,20 @@
<template>
<template v-if="message">
<a-alert :message="message" :type="type" />
<a-divider class="line" />
</template>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
defineProps({
message: {
type: String,
default: '',
},
type: {
type: String as PropType<'success' | 'info' | 'warning' | 'error'>,
default: 'error',
},
});
</script>

@ -0,0 +1,46 @@
<template>
<a-card>
<a-breadcrumb :routes="breadcrumb">
<template #itemRender="{ route }">
<span class="font14 color-666">
{{ route.breadcrumbName }}
</span>
</template>
</a-breadcrumb>
<h2 class="font18 marT13 rowSC link" @click="handleBreadcrumb">
<LeftOutlined />
<span class="marL10">{{ title }}</span>
</h2>
</a-card>
</template>
<script setup lang="ts">
import { LeftOutlined } from '@ant-design/icons-vue';
import { Route } from 'ant-design-vue/es/breadcrumb/Breadcrumb';
import { useBreadcrumbTitle } from '/@/hooks/useBreadcrumbTitle';
const { title } = useBreadcrumbTitle();
const router = useRouter();
const emits = defineEmits(['handleClick']);
const breadcrumb = computed(
() =>
router.currentRoute.value.matched
.filter((n) => !['/', '/app'].includes(n.path))
.map((item) => {
return {
path: item.path,
breadcrumbName: item.meta.title || '',
};
}) as Route[],
);
const handleBreadcrumb = () => {
emits('handleClick', breadcrumb.value);
};
</script>
<style scoped>
.link {
text-decoration: none;
}
</style>

@ -0,0 +1,54 @@
<template>
<icon-font :style="iconStyle" :class="iconClass" :type="iconType" />
</template>
<script lang="ts">
import { createFromIconfontCN } from '@ant-design/icons-vue';
import { ICONFONTURL } from '../../../config/constant';
const IconFont = createFromIconfontCN({
scriptUrl: ICONFONTURL,
});
export default defineComponent({
components: {
IconFont,
},
props: {
type: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
align: {
type: String,
default: '',
},
size: {
type: String,
default: '18px',
},
},
setup(props) {
return {
iconType: computed(() => `icon-a-SimpleChainlianmenglianjichubantubiao_${props.type}`),
iconClass: computed(() => (props.className ? `my-icon ${props.className}` : 'my-icon')),
iconStyle: computed(() => {
const style = {};
if (props.align) style['vertical-align'] = props.align;
if (props.size) style['font-size'] = props.size;
return style;
}),
};
},
});
</script>
<style scoped>
.my-icon {
font-size: 18px;
vertical-align: middle;
fill: currentColor;
}
</style>

@ -0,0 +1,49 @@
<template>
<a-modal :width="640" :visible="visible" @cancel="handleCancel" :footer="null">
<template #title>
<span class="font18">{{ title }}</span>
</template>
<slot></slot>
<a-row>
<a-col style="width: 110px" />
<a-col :span="16" class="rowE">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button :loading="loading" @click="handleSubmit" type="primary">{{ okText }}</a-button>
</a-space>
</a-col>
</a-row>
</a-modal>
</template>
<script lang="ts">
export default defineComponent({
props: {
title: {
type: String,
default: '',
},
visible: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
okText: {
type: String,
default: '创建',
},
},
emits: ['cancel', 'ok'],
setup(_, { emit }) {
const handleCancel = () => emit('cancel');
const handleSubmit = () => emit('ok');
return {
handleCancel,
handleSubmit,
};
},
});
</script>

@ -0,0 +1,220 @@
<template>
<div class="table-component">
<!-- filter -->
<TableFilter
:button="tableFilterButton"
:items="tableFilterItems"
:model="tableFilterModel"
:hiddenFilter="hiddenFilter"
:pagination="null"
@onSearch="onSearch"
/>
<!-- table -->
<a-table
:class="['ant-table-striped', { border: hasBordered }]"
:rowClassName="(_, index) => (index % 2 === 1 ? 'table-striped' : '')"
:dataSource="dataSource"
:columns="columns"
:rowKey="(record) => record.id"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
:scroll="scroll"
>
<!-- slot 写法自定义 操作列 -->
<!-- <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
</template> -->
<template #bodyCell="{ column, text, index, record }">
<template v-if="column.key === 'toIndex'">
<span>{{ index + 1 }}</span>
</template>
<template v-if="column.key === 'toDate'">
<span>{{ text ? formatDate(text) : '-' }}</span>
</template>
<template v-if="column.key === 'toDateTime'">
<span>{{ text ? formatDate(text, 'time') : '-' }}</span>
</template>
<!-- 函数式写法自定义 操作列 -->
<template v-if="column.key === 'action'">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<!-- 气泡确认框 -->
<a-popconfirm
v-if="action.enable"
:title="action?.title"
@confirm="action?.onConfirm(record)"
@cancel="action?.onCancel(record)"
>
<a @click.prevent="() => {}" :type="action.type">{{ action.label }}</a>
</a-popconfirm>
<span v-else-if="!action.permission"></span>
<!-- 按钮 -->
<a v-else @click="action?.onClick(record)" :type="action.type">{{ action.label }}</a>
<!-- 分割线 -->
<a-divider type="vertical" v-if="index < getActions.length - 1" />
</template>
</template>
</template>
</a-table>
</div>
</template>
<script lang="ts">
import { FilterValue } from 'ant-design-vue/es/table/interface';
import dayjs from 'dayjs';
import { usePagination } from 'vue-request';
import { formatToDate, formatToDateTime } from '/@/utils/dateUtil';
import { usePermission } from '/@/hooks/usePermission';
import { useRole } from '/@/hooks/useRole';
import { TablePaginationConfig } from 'ant-design-vue/lib/table/interface';
// const req = () => new Promise((resolve) => resolve({ total: 0, list: [] }));
export default defineComponent({
props: [
'bordered',
'hiddenFilter',
'url' /* 请求接口 promise */,
'columns' /* Table组件:columns 不包含操作列 */,
'actions' /* Table组件:操作列 */,
'button' /* Filter筛选列组件:交互按钮 */,
'items' /* Filter筛选列组件:包含的项 */,
'model' /* Filter筛选列组件:form model */,
'resKey',
'scroll',
],
// emits: ['onSearch'],
setup(props) {
const { hasPermission } = usePermission();
const { hasRole } = useRole();
const {
data: dataSource,
run,
loading,
current,
pageSize,
total,
refresh,
} = usePagination(props.url, {
// formatResult: (res: any) => (props.resKey ? res[props.resKey.list] : res.list),
pagination: {
pageSizeKey: 'limit',
currentKey: 'page',
},
});
const hasBordered = computed(() => props.bordered ?? true);
const listData = computed(
() => (dataSource.value as unknown as Indexable)?.[props?.resKey?.list || 'list'] || [],
);
const pagination = computed(() => ({
total: total.value,
current: current.value,
pageSize: pageSize.value,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => h('span', {}, `${total.value}`),
}));
const handleTableChange = (
pag: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: any,
) => {
run({
limit: pag!.pageSize!,
page: pag?.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
});
};
// action
const getActions = computed(() => {
return (
(toRaw(props.actions) || [])
// .filter((action) => hasPermission(action.auth))
.map((action) => {
const { popConfirm } = action;
return {
type: 'link',
...action,
...(popConfirm || {}),
enable: !!popConfirm,
permission: hasPermission(action.auth) && hasRole(action.role),
};
})
);
});
// filter
const tableFilterModel = computed(() => props.model);
const tableFilterButton = computed(() => props.button);
const tableFilterItems = computed(() => props.items);
const onSearch = () => {
const args = toRaw(tableFilterModel.value) || {};
//
if (args) {
Object.keys(args).map((key) => {
if (args[key] && dayjs.isDayjs(args[key])) {
args[key] = formatToDate(args[key]);
}
});
}
run({ page: current.value, limit: pageSize.value, ...args });
};
//
const formatDate = (val: string, type: 'date' | 'time' = 'date') => {
const formatFn = type === 'date' ? formatToDate : formatToDateTime;
return val.length === 10 ? formatFn(Number(val) * 1000) : formatFn(val);
};
return {
dataSource: listData,
loading,
pagination,
hasBordered,
handleTableChange,
run,
refresh,
getActions,
// filter
tableFilterModel,
tableFilterButton,
tableFilterItems,
onSearch,
formatDate,
};
},
});
</script>
<style lang="less" scoped>
.ant-table-striped :deep(.table-striped) td {
background-color: #fafafa;
}
.ant-table-striped :deep(.ant-table-pagination.ant-pagination) {
margin: 30px auto;
width: 100%;
text-align: center;
.ant-pagination-prev,
.ant-pagination-next {
.anticon {
vertical-align: 1.5px;
}
}
}
.ant-table-striped :deep(.ant-pagination-item-active) {
background: #3860f4;
a {
color: #ffffff;
}
}
.border {
border: 0.5px solid rgba(210, 210, 210, 0.5);
}
</style>

@ -0,0 +1,132 @@
<template>
<a-card v-if="!hasHidden" :body-style="{ padding: '0 0 24px 0' }" :bordered="false">
<a-form class="form-container" layout="horizontal" :model="formModel">
<a-row type="flex">
<a-col v-if="button" flex="100px">
<span class="text" v-if="button.type === 'text'">{{ button.label }}</span>
<!-- <a-button v-else v-bind="button" v-auth="button.auth" @click="button?.onClick">{{
button.label
}}</a-button> -->
<a-button v-if="getButton.permission" v-bind="button" @click="button?.onClick">{{
button.label
}}</a-button>
</a-col>
<a-col flex="auto" class="rowE">
<a-space>
<template v-for="item in getItems" :key="item.name">
<a-form-item :name="item.name">
<a-select
v-if="item.type === 'select'"
:key="`select-${item.name}`"
v-bind="item"
v-model:value="formModel[item.name]"
class="default-select-w"
/>
<a-date-picker
v-else-if="item.type === 'date'"
:key="`date-${item.name}`"
v-bind="item"
v-model:value="formModel[item.name]"
class="default-select-w"
/>
<a-input-search
v-else
v-bind="item"
:key="`input-${item.name}`"
v-model:value="formModel[item.name]"
@search="handleSubmit"
class="default-search-w"
>
<template #enterButton>
<a-button type="primary">查询</a-button>
</template>
</a-input-search>
</a-form-item>
</template>
</a-space>
</a-col>
</a-row>
</a-form>
</a-card>
</template>
<script lang="ts">
import { usePermission } from '/@/hooks/usePermission';
import { useRole } from '/@/hooks/useRole';
export default defineComponent({
props: ['hiddenFilter', 'button', 'items', 'model'],
emits: ['onSearch'],
setup(props, { emit }) {
const { hasPermission } = usePermission();
const { hasRole } = useRole();
const formModel = reactive(props.model || {});
const getItems = computed(() => {
return (props.items || []).map((item) => {
return {
type: 'input',
...item,
};
});
});
const handleSubmit = () => {
emit('onSearch');
};
// onMounted(() => console.log(`hiddenFilter`, props.hiddenFilter));
const hasHidden = ref(props.hiddenFilter);
watchEffect(() => {
//
if (!props.button && !props.items) {
hasHidden.value = true;
}
});
const getButton = computed(() => {
const plainObj = toRaw(props.button) || {};
return {
...plainObj,
permission: hasPermission(plainObj.auth) && hasRole(plainObj.role),
};
});
return {
formModel,
getItems,
hasHidden,
getButton,
handleSubmit,
};
},
});
</script>
<style lang="less" scoped>
.form-container {
.default-select-w {
width: 120px;
}
.default-search-w {
width: 290px;
}
// & :deep(.ant-input) {
// height: 36px;
// }
// & :deep(.ant-select) {
// height: 36px;
// }
// & :deep(.ant-btn) {
// height: 36px;
// }
}
.text {
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: rgba(0, 0, 0, 0.85);
}
</style>

@ -0,0 +1,96 @@
<template>
<div>
<a-upload
name="file"
v-model:file-list="fileList"
v-bind="uploadCof"
@change="handleChange"
:accept="accept"
>
<a-button v-if="fileList.length < 1">
<upload-outlined style="vertical-align: 2px" />
上传文件
</a-button>
</a-upload>
</div>
</template>
<script setup lang="ts">
import { useMessage } from '/@/hooks/useMessage';
import { UploadOutlined } from '@ant-design/icons-vue';
const { createMessage } = useMessage();
// props
const props = defineProps({
accept: {
type: String,
default: () => {
return '.doc';
},
},
size: {
type: Number,
default: () => {
return null;
},
},
});
// emits
const emits = defineEmits(['uploadRemoved', 'uploadSuccess']);
// uploadCof
const uploadCof = reactive({
headers: {
Authorization: localStorage.getItem('x-auth-token'),
},
action: location.origin + '/api/v1/upload',
});
const fileList = ref([]);
const bytesToSize = (bytes = 0) => {
return bytes === 0 ? 0 : bytes / 1024 / 1024;
};
const handleChange = (info) => {
//
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
if (props.size) {
if (props.size < bytesToSize(info.file.size)) {
createMessage.warn('文件大小不能超过' + props.size + 'M');
return false;
}
} else if (info.fileList.length > 1) {
info.fileList.length = 1;
createMessage.warn('一次仅可以上传一个文件,请完成这次操作之后再上传');
return false;
} else if (info.file.status === 'removed') {
createMessage.success('删除成功,请上传新的合约文件');
info.fileList.length = 0;
emits('uploadRemoved', '');
fileList.value = [];
return false;
}
}
//
if (info.file.status === 'done') {
if (info.file.type.indexOf('image') !== -1) {
createMessage.error('请上传正确的格式类型');
info.fileList.length = 0;
return false;
}
emits('uploadSuccess', {
data: info.file.response.result,
fileType: info.file.name,
});
createMessage.success(`${info.file.name} 上传成功`);
} else if (info.file.status === 'error') {
info.fileList.length = 0;
createMessage.error(`${info.file.name} 上传失败,请稍后再试`);
}
};
</script>

@ -0,0 +1,11 @@
/**
* Configure and register global directives
*/
import type { App } from 'vue';
import { setupPermissionDirective } from './permission';
import { setupRoleDirective } from './role';
export function setupGlobDirectives(app: App) {
setupPermissionDirective(app);
setupRoleDirective(app);
}

@ -0,0 +1,32 @@
/**
* Global authority directive
* Used for fine-grained control of component permissions
* @Example v-auth="RoleEnum.TEST"
*/
import type { App, Directive, DirectiveBinding } from 'vue';
import { usePermission } from '/@/hooks/usePermission';
function isAuth(el: Element, binding: any) {
const { hasPermission } = usePermission();
const value = binding.value;
if (!value) return;
if (!hasPermission(value)) {
el.parentNode?.removeChild(el);
}
}
const mounted = (el: Element, binding: DirectiveBinding<any>) => {
isAuth(el, binding);
};
const authDirective: Directive = {
mounted,
};
export function setupPermissionDirective(app: App) {
app.directive('auth', authDirective);
}
export default authDirective;

@ -0,0 +1,51 @@
/**
* Global authority directive
* 0 / 1
* @Example v-role="role"
*/
import type { App, Directive, DirectiveBinding } from 'vue';
import { useRole } from '/@/hooks/useRole';
import intersection from 'lodash-es/intersection';
// 操作按钮无权限时,替换展示内容
function replaceHtml(parentNode: HTMLElement | null) {
if (!parentNode) return;
const child = document.createElement('span');
// 只过滤 Table里的操作按钮
const classNames = ['ant-space-item', 'ant-table-row-cell-break-word'];
const parentNodeText =
intersection(classNames, parentNode?.className?.split(' ')).length > 0 ? '——' : '';
// console.dir(parentNode);
child.innerHTML = parentNodeText;
child.style.color = 'rgba(0,0,0,.08)';
parentNode?.appendChild(child);
}
function isAuth(el: Element, binding: any) {
const { hasRole } = useRole();
const value = binding.value;
// 过滤 undefined、null
if (value == null) return;
// 权限验证
if (!hasRole(value)) {
const parentNode = el.parentNode;
el.parentNode?.removeChild(el);
replaceHtml(parentNode as any);
}
}
const mounted = (el: Element, binding: DirectiveBinding<any>) => {
isAuth(el, binding);
};
const authDirective: Directive = {
mounted,
};
export function setupRoleDirective(app: App) {
app.directive('role', authDirective);
}
export default authDirective;

@ -0,0 +1,27 @@
/**
* @name AuthEnum
* @description v-auth 使
* @Example v-auth="AuthEnum.user_create"
*/
export enum AuthEnum {
/**
*
*/
// 新增用户
user_create = '/v1/user/create',
// 编辑用户
user_update = '/v1/user/update',
// 删除用户
user_delete = '/v1/user/delete',
/**
*
*/
// 新增角色
role_create = '/v1/role/create',
// 修改角色
role_update = '/v1/role/update',
// 删除角色
role_delete = '/v1/role/delete',
}

@ -0,0 +1,38 @@
/**
* @name useBreadcrumbTitle
* @description Title
* @param isAddOn on注册
*/
import mitt from '/@/utils/mitt';
import { onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const emitter = mitt();
const key = Symbol();
export const useBreadcrumbTitle = (isAddOn = true) => {
const route = useRoute();
const title = ref(route.meta.title);
watch(
() => route.meta.title,
(val) => {
title.value = val;
},
);
const changeTitle = (val: string) => (title.value = val);
onMounted(() => isAddOn && emitter.on(key, changeTitle));
onUnmounted(() => isAddOn && emitter.off(key, changeTitle));
const setBreadcrumbTitle = (title: string) => emitter.emit(key, title);
return {
title,
setBreadcrumbTitle,
};
};

@ -0,0 +1,108 @@
import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import { useTimeoutFn } from '/@/hooks/useTimeout';
import { Fn, tryOnUnmounted } from '@vueuse/core';
import { unref, nextTick, computed, ref } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { useEventListener } from '/@/hooks/useEventListener';
import echarts from '/@/utils/echarts';
export function useECharts(
elRef: Ref<HTMLDivElement>,
theme: 'light' | 'dark' | 'default' = 'default',
) {
const getDarkMode = computed(() => {
return theme;
});
let chartInstance: echarts.ECharts | null = null;
let resizeFn: Fn = resize;
const cacheOptions = ref({}) as Ref<EChartsOption>;
let removeResizeFn: Fn = () => {};
resizeFn = useDebounceFn(resize, 200);
const getOptions = computed(() => {
return {
backgroundColor: 'transparent',
...cacheOptions.value,
} as EChartsOption;
});
function initCharts() {
const el = unref(elRef);
if (!el || !unref(el)) {
return;
}
chartInstance = echarts.init(el);
const { removeEvent } = useEventListener({
el: window,
name: 'resize',
listener: resizeFn,
});
removeResizeFn = removeEvent;
if (el.offsetHeight === 0) {
useTimeoutFn(() => {
resizeFn();
}, 30);
}
}
function setOptions(options: EChartsOption, clear = true) {
cacheOptions.value = options;
if (unref(elRef)?.offsetHeight === 0) {
useTimeoutFn(() => {
setOptions(unref(getOptions));
}, 30);
return;
}
nextTick(() => {
useTimeoutFn(() => {
if (!chartInstance) {
initCharts();
if (!chartInstance) return;
}
clear && chartInstance?.clear();
chartInstance?.setOption(unref(getOptions));
}, 30);
});
}
function resize() {
chartInstance?.resize();
}
watch(
() => getDarkMode.value,
() => {
if (chartInstance) {
chartInstance.dispose();
initCharts();
setOptions(cacheOptions.value);
}
},
);
tryOnUnmounted(() => {
if (!chartInstance) return;
removeResizeFn();
chartInstance.dispose();
chartInstance = null;
});
function getInstance(): echarts.ECharts | null {
if (!chartInstance) {
initCharts();
}
return chartInstance;
}
return {
setOptions,
resize,
echarts,
getInstance,
};
}

@ -0,0 +1,58 @@
import type { Ref } from 'vue';
import { ref, watch, unref } from 'vue';
import { useThrottleFn, useDebounceFn } from '@vueuse/core';
export type RemoveEventFn = () => void;
export interface UseEventParams {
el?: Element | Ref<Element | undefined> | Window | any;
name: string;
listener: EventListener;
options?: boolean | AddEventListenerOptions;
autoRemove?: boolean;
isDebounce?: boolean;
wait?: number;
}
export function useEventListener({
el = window,
name,
listener,
options,
autoRemove = true,
isDebounce = true,
wait = 80,
}: UseEventParams): { removeEvent: RemoveEventFn } {
/* eslint-disable-next-line */
let remove: RemoveEventFn = () => {};
const isAddRef = ref(false);
if (el) {
const element = ref(el as Element) as Ref<Element>;
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
const realHandler = wait ? handler : listener;
const removeEventListener = (e: Element) => {
isAddRef.value = true;
e.removeEventListener(name, realHandler, options);
};
const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options);
const removeWatch = watch(
element,
(v, _ov, cleanUp) => {
if (v) {
!unref(isAddRef) && addEventListener(v);
cleanUp(() => {
autoRemove && removeEventListener(v);
});
}
},
{ immediate: true },
);
remove = () => {
removeEventListener(element.value);
removeWatch();
};
}
return { removeEvent: remove };
}

@ -0,0 +1,125 @@
import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
import { Modal, message as Message, notification } from 'ant-design-vue';
import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons-vue';
import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification';
import { isString } from '/@/utils/is';
// 手动引入 message样式
import 'ant-design-vue/es/message/style';
import 'ant-design-vue/es/notification/style';
export interface NotifyApi {
info(config: NotificationArgsProps): void;
success(config: NotificationArgsProps): void;
error(config: NotificationArgsProps): void;
warn(config: NotificationArgsProps): void;
warning(config: NotificationArgsProps): void;
open(args: NotificationArgsProps): void;
close(key: String): void;
config(options: ConfigProps): void;
destroy(): void;
}
export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
export declare type IconType = 'success' | 'info' | 'error' | 'warning';
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
iconType: 'warning' | 'success' | 'error' | 'info';
}
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
interface ConfirmOptions {
info: ModalFunc;
success: ModalFunc;
error: ModalFunc;
warn: ModalFunc;
warning: ModalFunc;
}
function getIcon(iconType: string) {
if (iconType === 'warning') {
return <InfoCircleFilled class="modal-icon-warning" />;
} else if (iconType === 'success') {
return <CheckCircleFilled class="modal-icon-success" />;
} else if (iconType === 'info') {
return <InfoCircleFilled class="modal-icon-info" />;
} else {
return <CloseCircleFilled class="modal-icon-error" />;
}
}
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
if (isString(content)) {
return <div innerHTML={`<div>${content as string}</div>`}></div>;
} else {
return content;
}
}
/**
* @description: Create confirmation box
*/
function createConfirm(options: ModalOptionsEx): ConfirmOptions {
const iconType = options.iconType || 'warning';
Reflect.deleteProperty(options, 'iconType');
const opt: ModalFuncProps = {
centered: true,
icon: getIcon(iconType),
...options,
content: renderContent(options),
};
return Modal.confirm(opt) as unknown as ConfirmOptions;
}
const getBaseOptions = () => {
return {
okText: '确定',
centered: true,
};
};
function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial {
return {
...getBaseOptions(),
...options,
content: renderContent(options),
icon: getIcon(icon),
};
}
function createSuccessModal(options: ModalOptionsPartial) {
return Modal.success(createModalOptions(options, 'success'));
}
function createErrorModal(options: ModalOptionsPartial) {
return Modal.error(createModalOptions(options, 'close'));
}
function createInfoModal(options: ModalOptionsPartial) {
return Modal.info(createModalOptions(options, 'info'));
}
function createWarningModal(options: ModalOptionsPartial) {
return Modal.warning(createModalOptions(options, 'warning'));
}
notification.config({
placement: 'topRight',
duration: 3,
});
/**
* @description: message
*/
export function useMessage() {
return {
createMessage: Message,
notification: notification as NotifyApi,
createConfirm: createConfirm,
createSuccessModal,
createErrorModal,
createInfoModal,
createWarningModal,
};
}

@ -0,0 +1,35 @@
/**
* @name usePermission
* @description
*/
import intersection from 'lodash-es/intersection';
import { isArray } from '../utils/is';
import { usePermissioStore } from '/@/store/modules/permission';
export function usePermission() {
const permissioStore = usePermissioStore();
function hasPermission(value?: string | string[], def = true): boolean {
// Visible by default
if (!value) {
return def;
}
if (permissioStore.getIsAdmin === 1) {
return true;
}
if (!isArray(value)) {
return permissioStore.getAuths?.includes(value);
}
if (isArray(value)) {
return intersection(value, permissioStore.getAuths).length > 0;
}
return true;
}
return { hasPermission };
}

@ -0,0 +1,27 @@
/**
* @name useRole
* @description
*/
import { usePermissioStore } from '/@/store/modules/permission';
export function useRole() {
const permissioStore = usePermissioStore();
function hasRole(value?: string | string[], def = true): boolean {
if (value == null) {
return def;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return permissioStore.getRole === value;
}
return def;
}
return { hasRole };
}

@ -0,0 +1,45 @@
import { ref, watch } from 'vue';
import { Fn, tryOnUnmounted } from '@vueuse/core';
import { isFunction } from '/@/utils/is';
export function useTimeoutFn(handle: Fn, wait: number, native = false) {
if (!isFunction(handle)) {
throw new Error('handle is not Function!');
}
const { readyRef, stop, start } = useTimeoutRef(wait);
if (native) {
handle();
} else {
watch(
readyRef,
(maturity) => {
maturity && handle();
},
{ immediate: false },
);
}
return { readyRef, stop, start };
}
export function useTimeoutRef(wait: number) {
const readyRef = ref(false);
let timer: TimeoutHandle;
function stop(): void {
readyRef.value = false;
timer && window.clearTimeout(timer);
}
function start(): void {
stop();
timer = setTimeout(() => {
readyRef.value = true;
}, wait);
}
start();
tryOnUnmounted(stop);
return { readyRef, stop, start };
}

@ -0,0 +1,23 @@
import { watch, unref } from 'vue';
import { useTitle as usePageTitle } from '@vueuse/core';
import { useRouter } from 'vue-router';
/**
* Listening to page changes and dynamically changing site titles
*/
export function useTitle() {
const { currentRoute } = useRouter();
const pageTitle = usePageTitle();
watch(
[() => currentRoute.value.path],
() => {
const route = unref(currentRoute);
const tTitle = route?.meta?.title as string;
pageTitle.value = tTitle;
},
{ immediate: true },
);
}

@ -0,0 +1,50 @@
<template>
<a-layout-header class="header">
<div class="logo-wrap">
<router-link :to="{ path: '/' }">
<img :src="logo" class="logo" />
<h1 class="title"> {{ APP_TITLE }} </h1>
<!-- <span class="subTitle">基础版</span> -->
</router-link>
</div>
<div>
<RightContent />
</div>
</a-layout-header>
</template>
<script setup lang="ts">
import RightContent from './RightContent.vue';
import logo from '/@/assets/images/logo.png';
import { APP_TITLE } from '../../../../config/constant';
</script>
<style lang="less" scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
height: 80px;
padding: 0 16px;
background: #ffffff;
box-shadow: 0 1px 4px #00152914;
z-index: 1;
.logo-wrap {
height: 100%;
a {
display: flex;
align-items: center;
height: 100%;
}
.title {
margin: 0;
margin-left: 12px;
font-size: 18px;
color: #000000;
}
.logo {
width: 48px;
height: 48px;
}
}
}
</style>

@ -0,0 +1,88 @@
<template>
<div class="sys-setting">
<a-dropdown placement="bottom">
<template #overlay>
<a-menu :selectedKeys="selectedKeys" class="menu-box">
<a-menu-item v-for="item in navs" :key="item.path" @click="handleRoute(item?.path)">
<template #icon>
<Icon align="1px" size="20px" :type="item.icon" />
</template>
<span>{{ item.name }}</span>
</a-menu-item>
</a-menu>
</template>
<Space class="wrap" align="baseline" direction="horizontal">
<Icon align="2px" type="xitongshezhi" />
<span class="setting">系统设置</span>
<Icon align="2px" type="xialajiantou" />
</Space>
</a-dropdown>
</div>
</template>
<script setup lang="ts">
import { Space } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user';
import { navs as myNavs } from './constant';
import { usePermissioStore } from '/@/store/modules/permission';
const store = useUserStore();
const permissioStore = usePermissioStore();
const router = useRouter();
const navs = ref(myNavs);
const selectedKeys = ref<string[]>([]);
watchEffect(() => {
const modules = permissioStore.getModules;
if (modules.length && permissioStore.getIsAdmin === 0) {
navs.value = unref(navs).filter((n) => (n.auth ? modules.includes(n.auth) : true));
}
});
watchEffect(() => {
if (router.currentRoute) {
const matched = router.currentRoute.value.matched.concat();
selectedKeys.value = matched.filter((r) => r.name !== 'index').map((r) => r.path);
}
});
const handleRoute = (path?: string) => {
if (path) return router.push(path);
// 退
store.logout();
};
</script>
<style lang="less" scoped>
.sys-setting {
height: 100%;
display: flex;
justify-content: center;
padding-right: 16px;
.wrap {
height: 55px;
.setting {
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: rgba(0, 0, 0, 0.85);
margin: 0 8px 0 4px;
}
}
.my-icon {
font-size: 18px !important;
}
}
.menu-box :deep(.ant-dropdown-menu-item) {
width: 142px;
height: 42px;
line-height: 42px;
padding: 0 16px;
}
.menu-box :deep(.ant-dropdown-menu-item-selected) {
background: #eaeffe;
color: #3860f4;
}
</style>

@ -0,0 +1,125 @@
import { Layout, Menu, Space } from 'ant-design-vue';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
import Icon from '/@/components/Icon/index.vue';
import { PropType, h, Transition } from 'vue';
import { MenuDataItem } from '../utils/typings';
import { router } from '/@/router';
import './index.less';
export default defineComponent({
name: 'BaseMenu',
props: {
theme: {
type: String,
default: 'light',
},
menuWidth: {
type: Number,
default: 208,
},
menuData: {
type: Array as PropType<MenuDataItem[]>,
default: () => [],
},
},
setup(props) {
const state = reactive<any>({
collapsed: false, // default value
openKeys: [],
selectedKeys: [],
});
watchEffect(() => {
if (router.currentRoute) {
const matched = router.currentRoute.value.matched.concat();
state.selectedKeys = matched.filter((r) => r.name !== 'index').map((r) => r.path);
state.openKeys = matched
.filter((r) => r.path !== router.currentRoute.value.path)
.map((r) => r.path);
}
});
const onSelect = (e: { key: string; item: { props: { routeid: number } } } | any) => {
router.push(e.key);
};
const getIcon = (type?: string) =>
type ? <Icon type={type} className="sideMenu-icon" /> : null;
// 构建树结构
const makeTreeDom = (data: MenuDataItem[]): JSX.Element[] => {
return data.map((item: MenuDataItem) => {
if (item.children) {
return (
<Menu.SubMenu
key={item.path}
title={
<>
{getIcon(item.meta?.icon as string)}
<span>{item.meta?.title}</span>
</>
}
>
{makeTreeDom(item.children)}
</Menu.SubMenu>
);
}
return (
<Menu.Item key={item.path}>
{getIcon(item.meta?.icon as string)}
<span>{item.meta?.title}</span>
</Menu.Item>
);
});
};
return () => {
return (
<Layout.Sider
width={208}
collapsedWidth={54}
class="my-sideMenu-sider"
theme="light"
trigger={null}
breakpoint="lg"
onBreakpoint={(val) => (state.collapsed = val)}
collapsible
collapsed={state.collapsed}
// collapsedWidth={48}
>
{/* logo */}
<Transition name="fade-top">
{!state.collapsed && (
<div class="my-sideMenu-sider_logo">
<Space align="center" class="link">
<Icon type="guanlipingtai" size="20px" align="0px" />
<span class="font16 nowrap"></span>
</Space>
</div>
)}
</Transition>
{/* menu */}
<Menu
theme="light"
mode="inline"
selectedKeys={state.selectedKeys}
{...(state.collapsed ? {} : { openKeys: state.openKeys })}
onOpenChange={(keys: any[]) => (state.openKeys = keys)}
onSelect={onSelect}
class="my-sideMenu-sider_menu"
>
{makeTreeDom(props.menuData)}
</Menu>
{/* footer */}
<div class="my-sideMenu-sider_footer">
{h(state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
style: { fontSize: 16 },
onClick: () => (state.collapsed = !state.collapsed),
})}
</div>
</Layout.Sider>
);
};
},
});

@ -0,0 +1,31 @@
export const navs = [
// {
// icon: 'yonghuguanli',
// name: '用户管理',
// path: '/sys/user',
// auth: 'user',
// },
// {
// icon: 'jiaoseguanli',
// name: '角色管理',
// path: '/sys/role',
// auth: 'role',
// },
// {
// icon: 'xitongrizhi',
// name: '系统日志',
// path: '/sys/logs',
// auth: 'log',
// },
{
icon: 'zhanghaozhongxin',
name: '账号中心',
path: '/sys/account',
auth: '',
},
{
icon: 'tuichudenglu_huaban1fuben17',
name: '退出登录',
auth: '',
},
];

@ -0,0 +1,30 @@
.my-sideMenu-sider {
&_logo {
margin: 16px 0;
padding: 0 16px;
height: 23px;
overflow: hidden;
}
&_menu {
flex: 1 1 0%;
height: calc(100% - 55px);
overflow: hidden auto;
}
&_footer {
border-top: 1px solid #f0f0f0;
width: 100%;
position: absolute;
left: 0;
bottom: 0;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
.trigger:hover {
color: #3860f4;
}
}
.sideMenu-icon {
vertical-align: 1px;
}
}

@ -0,0 +1,50 @@
<template>
<div class="video-box">
<video
class="video-background"
preload="auto"
loop
playsinline
autoplay
src="https://v.api.aa1.cn/api/api-fj/video/0089.mp4%20%E5%AE%98%E7%BD%91api.aa1.cn%E5%85%8D%E8%B4%B9%E8%A7%86%E9%A2%91API.mp4"
tabindex="-1"
:muted="muted"
></video>
</div>
</template>
<script setup lang="ts">
import { get } from '/@/utils/http';
let muted = ref(true);
const refreshBackgroundURl = () => {
let getVideoUrl = 'http://localhost:3000/api/aaData/data';
get({ url: getVideoUrl }).then((res: any) => {
console.log(res);
});
};
onBeforeMount(() => refreshBackgroundURl());
</script>
<style lang="less" scoped>
.video-box {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #c1cff7;
/*进行视频裁剪*/
overflow: hidden;
}
.video-box .video-background {
position: absolute;
left: 50%;
top: 50%;
/*保证视频内容始终居中*/
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
/*保证视频充满屏幕*/
object-fit: cover;
min-height: 800px;
}
</style>

@ -0,0 +1,31 @@
<template>
<a-layout class="basicLayout-content">
<a-layout-content>
<a-card>
<router-view v-slot="{ Component, route }">
<transition name="fade-slide" mode="out-in" appear>
<div :key="route.name">
<component :is="Component" />
</div>
</transition>
</router-view>
</a-card>
</a-layout-content>
</a-layout>
<VideoBox></VideoBox>
</template>
<script setup lang="ts">
import VideoBox from './components/videoBox.vue';
</script>
<style lang="less" scoped>
.basicLayout-content {
height: calc(100vh);
width: calc(100vw - 200px);
max-width: 1280px;
margin: 0 auto;
overflow-y: auto;
padding: 24px;
background: rgba(32, 33, 333, 0.4);
opacity: 0.8;
}
</style>

@ -0,0 +1,47 @@
import type { RouteRecord, RouteRecordRaw } from 'vue-router';
type IRouteRecordRaw = RouteRecordRaw & { childrenPaths?: string[] };
// 过滤路由属性 hideInMenu hideChildInMenu
export function clearMenuItem(menusData: RouteRecord[] | RouteRecordRaw[]): RouteRecordRaw[] {
const filterHideMenus = menusData
.map((item: RouteRecord | RouteRecordRaw) => {
const finalItem = { ...item };
if (!finalItem.name || finalItem.meta?.hideInMenu) {
return null;
}
if (finalItem && finalItem?.children) {
if (
!finalItem.meta?.hideChildInMenu &&
finalItem.children.some(
(child: RouteRecord | RouteRecordRaw) => child && child.name && !child.meta?.hideInMenu,
)
) {
return {
...item,
children: clearMenuItem(finalItem.children),
};
}
delete finalItem.children;
}
return finalItem;
})
.filter((item) => item) as IRouteRecordRaw[];
//
return filterHideMenus;
}
// 存在二级菜单时,过滤掉重复的并在一级菜单显示的 item
export const filterRoutes = (menusData: RouteRecordRaw[]): RouteRecordRaw[] => {
const filterRoutes: string[] = [];
menusData.forEach((n) => {
if (n.children) {
n.children.forEach(({ path }) => filterRoutes.push(path));
}
});
return menusData.filter(({ path }) => !filterRoutes.includes(path));
};

@ -0,0 +1,59 @@
import type { VNode } from 'vue';
export type WithFalse<T> = T | false;
export type TargetType = '_blank' | '_self' | unknown;
export interface MetaRecord {
/**
* @name icon
*/
icon?: string | VNode;
/**
* @type children 'group'
*/
type?: string;
/**
* @name key
*/
title?: string;
/**
* @name
*/
authority?: string | string[];
/**
* @name '_blank' | '_self' | null | undefined
*/
target?: TargetType;
/**
* @name
*/
hideChildInMenu?: boolean;
/**
* @name
*/
hideInMenu?: boolean;
/**
* @name disable
*/
disabled?: boolean;
/**
* @name
*/
flatMenu?: boolean;
[key: string]: any;
}
export interface MenuDataItem {
/**
* @name path
*/
path: string;
name?: string | symbol;
meta?: MetaRecord;
/**
* @name
*/
children?: MenuDataItem[];
}

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

@ -0,0 +1,30 @@
// import 'ant-design-vue/dist/antd.css';
import 'sanitize.css';
// import 'sanitize.css/forms.css';
// import 'sanitize.css/typography.css';
import '/@/styles/index.less';
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router';
import { store } from './store';
import { setupGlobDirectives } from './directives';
import './router/permission';
// import { setupComponents } from './plugin';
const app = createApp(App);
app.use(store);
app.use(router);
// Register global directive
setupGlobDirectives(app);
// Register UI components
// setupComponents(app);
// 全局属性
// app.config.globalProperties.AuthEnum = AuthEnum;
app.mount('#app');

@ -0,0 +1,62 @@
/**
*
* unplugin-vue-components插件的自动引入性能问题
*/
import {
Alert,
Avatar,
Breadcrumb,
Button,
Card,
Col,
DatePicker,
Divider,
Dropdown,
Form,
Input,
Layout,
Menu,
Popconfirm,
Row,
Select,
Space,
Spin,
Table as AntdTable,
} from 'ant-design-vue';
import type { App } from 'vue';
import Icon from '/@/components/Icon/index.vue';
import Modal from '/@/components/Modal/index.vue';
import Table from '/@/components/Table/index.vue';
import TableFilter from '/@/components/TableFilter/index.vue';
import Upload from '/@/components/Upload/index.vue';
export function setupComponents(app: App<Element>) {
app.component('Icon', Icon);
app.component('Modal', Modal);
app.component('Table', Table);
app.component('TableFilter', TableFilter);
app.component('Upload', Upload);
app
.use(Alert)
.use(Avatar)
.use(Breadcrumb)
.use(Button)
.use(Card)
.use(Col)
.use(DatePicker)
.use(Divider)
.use(Dropdown)
.use(Form)
.use(Input)
.use(Layout)
.use(Menu)
.use(Popconfirm)
.use(Row)
.use(Select)
.use(Space)
.use(Spin)
.use(AntdTable);
}

@ -0,0 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router';
import routes from './router.config';
// app router
export const router = createRouter({
// 解决 二级路径存在时,路径地址路由不匹配的问题
// https://juejin.cn/post/7051826951463370760#heading-27
history: createWebHistory(import.meta.env.BASE_URL),
routes,
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
});

@ -0,0 +1,49 @@
/**
* @name permission
* @description
*/
import { router } from '.';
import { getToken } from '../utils/auth';
import { usePermissioStoreWithOut } from '/@/store/modules/permission';
const permissioStore = usePermissioStoreWithOut();
const whiteList = ['/login']; // no redirect whitelist
// router.beforeEach(async (to: any, _, next) => {
// next();
// // const hasToken = getToken();
// // if (hasToken) {
// // // 已登录
// // if (to.path === '/login') {
// // next({ path: '/' });
// // } else {
// // //是否获取过用户信息
// // const isGetUserInfo = permissioStore.getIsGetUserInfo;
// // if (isGetUserInfo) {
// // next();
// // } else {
// // // 没有获取,请求数据
// // await permissioStore.fetchAuths();
// // // 过滤权限路由
// // const routes = await permissioStore.buildRoutesAction();
// // // 404 路由一定要放在 权限路由后面
// // routes.forEach((route) => {
// // router.addRoute(route);
// // });
// // // hack 方法
// // // 不使用 next() 是因为,在执行完 router.addRoute 后,
// // // 原本的路由表内还没有添加进去的路由,会 No match
// // // replace 使路由从新进入一遍,进行匹配即可
// // next({ ...to, replace: true });
// // }
// // }
// // } else {
// // // 未登录
// // if (whiteList.indexOf(to.path) !== -1) {
// // next();
// // } else {
// // next('/login');
// // }
// // }
// });

@ -0,0 +1,116 @@
import BasicLayout from '/@/layouts/BasicLayout/index.vue';
import BlankLayout from '/@/layouts/BlankLayout.vue';
import type { RouteRecordRaw } from 'vue-router';
export const accessRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: BasicLayout,
redirect: '/index',
meta: { title: '管理平台' },
children: [
{
path: '/index',
component: () => import('/@/views/home/index.vue'),
name: 'home',
meta: {
title: '首页',
icon: 'liulanqi',
// auth: ['home'],
},
},
// {
// path: '/app/website',
// name: 'website',
// component: () => import('/@/views/website/index.vue'),
// meta: {
// title: '网站管理',
// keepAlive: true,
// icon: 'jiedianguanli',
// auth: ['website'],
// },
// },
// {
// path: '/app/table-demo',
// name: 'table-demo',
// component: () => import('/@/views/table-demo/index.vue'),
// meta: {
// title: '表格用法',
// keepAlive: true,
// icon: 'rili',
// },
// },
// {
// path: '/app/others',
// name: 'others',
// component: BlankLayout,
// redirect: '/app/others/about',
// meta: {
// title: '其他菜单',
// icon: 'shurumimadenglu',
// auth: ['others'],
// },
// children: [
// {
// path: '/app/others/about',
// name: 'about',
// component: () => import('/@/views/others/about/index.vue'),
// meta: { title: '关于', keepAlive: true, hiddenWrap: true },
// },
// {
// path: '/app/others/antdv',
// name: 'antdv',
// component: () => import('/@/views/others/antdv/index.vue'),
// meta: { title: '组件', keepAlive: true, breadcrumb: true },
// },
// ],
// },
// {
// path: '/sys/account',
// name: 'account',
// component: () => import('/@/views/account/index.vue'),
// meta: { title: '用户管理', keepAlive: true, breadcrumb: true },
// },
],
},
];
const constantRoutes: RouteRecordRaw[] = [
// {
// path: '/login',
// component: () => import('/@/views/login/index.vue'),
// name: 'login',
// meta: { title: '登录' },
// },
...accessRoutes,
];
export const publicRoutes = [
{
path: '/redirect',
component: BlankLayout,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('/@/views/redirect/index'),
},
],
},
{
path: '/:pathMatch(.*)',
redirect: '/404',
},
{
path: '/404',
component: () => import('/@/views/404.vue'),
},
];
// /**
// * 基础路由
// * @type { *[] }
// */
// export const constantRouterMap = [];
export default constantRoutes;

@ -0,0 +1,5 @@
declare module '*.vue' {
import { defineComponent } from 'vue';
const Component: ReturnType<typeof defineComponent>;
export default Component;
}

@ -0,0 +1,9 @@
import { createPinia } from 'pinia';
const store = createPinia();
export { store };
// 因为 pinia的实现也是通过vue的各种api(ref/reactive/computed等)
// 所以,不要求一定要在Vue上挂载注册,可以随便在组件中使用,组件外使用也有对应方案
// 不过,app.use(store) 可以把store实例挂载到Vue上使用

@ -0,0 +1,45 @@
import { defineStore } from 'pinia';
import { store } from '/@/store';
import fetchApi from '/@/api/home';
import { ResInfoList } from '/@/api/home/model';
interface HomeState {
info: Nullable<ResInfoList>;
}
export const useHomeStore = defineStore({
id: 'app-home',
state: (): HomeState => ({
// info
info: null,
}),
getters: {
getInfo(): Nullable<ResInfoList> {
return this.info || null;
},
},
actions: {
setInfo(info: Nullable<ResInfoList>) {
this.info = info;
},
resetState() {
this.info = null;
},
/**
* @description: login
*/
async fetchInfo() {
const res = await fetchApi.info();
if (res) {
// save token
this.setInfo(res);
}
return res;
},
},
});
// Need to be used outside the setup
export function useHomeStoreWithOut() {
return useHomeStore(store);
}

@ -0,0 +1,134 @@
import { defineStore } from 'pinia';
import { store } from '/@/store';
import fetchApi from '/@/api/user';
import { RouteRecordRaw } from 'vue-router';
import constantRoutes, { accessRoutes, publicRoutes } from '/@/router/router.config';
import { filterAsyncRoutes } from '/@/utils/permission';
interface PermissioState {
isGetUserInfo: boolean; // 是否获取过用户信息
isAdmin: 0 | 1; // 是否为管理员
auths: string[]; // 当前用户权限
modules: string[]; // 模块权限
role: 0 | 1;
}
export const usePermissioStore = defineStore({
id: 'app-permission',
state: (): PermissioState => ({
// isGetUserInfo
isGetUserInfo: false,
// isAdmin
isAdmin: 0,
// auths
auths: [],
// modules
modules: [],
// role 0-银行 1-银保监
role: 0,
}),
getters: {
getAuths(): string[] {
return this.auths;
},
getRole(): 0 | 1 {
return this.role;
},
getModules(): string[] {
return this.modules;
},
getIsAdmin(): 0 | 1 {
return this.isAdmin;
},
getIsGetUserInfo(): boolean {
return this.isGetUserInfo;
},
},
actions: {
setAuth(auths: string[], modules: string[]) {
this.auths = auths;
this.isGetUserInfo = true;
this.modules = modules;
},
setIsAdmin(isAdmin: 0 | 1) {
this.isAdmin = isAdmin;
},
resetState() {
this.isGetUserInfo = false;
this.isAdmin = 0;
this.auths = [];
this.modules = [];
this.role = 0;
},
/**
* @name fetchAuths
* @description
*/
async fetchAuths() {
const res = await fetchApi.permission();
if (res) {
this.setAuth(res.auths, res.modules);
this.setIsAdmin(res.is_admin || 0);
}
return res;
},
/**
* @name buildRoutesAction
* @description:
*/
async buildRoutesAction(): Promise<RouteRecordRaw[]> {
// 404 路由一定要放在 权限路由后面
let routes: RouteRecordRaw[] = [...constantRoutes, ...accessRoutes, ...publicRoutes];
if (this.getIsAdmin !== 1) {
// 普通用户
// 1. 方案一:过滤每个路由模块涉及的接口权限,判断是否展示该路由
// 2. 方案二:直接检索接口权限列表是否包含该路由模块,不做细分,axios同一拦截
routes = [
...constantRoutes,
...filterAsyncRoutes(accessRoutes, this.modules),
...publicRoutes,
];
}
return routes;
},
// /**
// * @name buildRoutesAction
// * @description: 获取路由
// */
// buildRoutesAction(): RouteRecordRaw[] {
// // this.isGetUserInfo = true;
// this.setIsGetUserInfo(true);
// // 404 路由一定要放在 权限路由后面
// let routes: RouteRecordRaw[] = [...constantRoutes, ...accessRoutes, ...publicRoutes];
// // 1. 角色权限过滤:0-银行 1-银保监
// let filterRoutes = filterRouteByRole(cloneDeep(accessRoutes), this.role);
// // let filterRoutes = routes;
// // 2. 菜单权限过滤:
// // 管理员直接跳过
// if (this.getIsAdmin === 0) {
// const filterRoutesByAuth = filterAsyncRoutes(cloneDeep(filterRoutes), this.modules);
// filterRoutes = filterRoutesByAuth;
// }
// // 普通用户
// // 1. 方案一:过滤每个路由模块涉及的接口权限,判断是否展示该路由
// // 2. 方案二:直接检索接口权限列表是否包含该路由模块,不做细分,axios同一拦截
// routes = [...constantRoutes, ...filterRoutes, ...publicRoutes];
// return routes;
// },
},
});
// Need to be used outside the setup
export function usePermissioStoreWithOut() {
return usePermissioStore(store);
}

@ -0,0 +1,62 @@
/**
* @name sysAccount
* @description -
*/
import { defineStore } from 'pinia';
import { store } from '/@/store';
import fetchApi from '/@/api/sys/account';
import { ReqAccount, ResAccount } from '/@/api/sys/account/model';
// import { encryptByDES } from '/@/utils/crypto';
type AccountInfoTy = ResAccount | null;
interface UserState {
info: AccountInfoTy;
}
export const useSysAccountStore = defineStore({
id: 'sys-account',
state: (): UserState => ({
info: null,
}),
getters: {
getAccount(): AccountInfoTy {
return this.info;
},
},
actions: {
setAccount(info: AccountInfoTy) {
this.info = info;
},
resetState() {
this.info = null;
},
/**
* @description: fetchRole
*/
async fetchAccount() {
const res = await fetchApi.account();
if (res) {
this.setAccount(res);
}
return res !== undefined;
},
/**
* @description: fetchAccountUpdate
*/
async fetchAccountUpdate(params: ReqAccount) {
// if (params.password) {
// params.password = encryptByDES(params.password);
// }
const res = await fetchApi.update(params);
return res !== undefined;
},
},
});
// Need to be used outside the setup
export function useSysAccountStoreWithOut() {
return useSysAccountStore(store);
}

@ -0,0 +1,69 @@
import { defineStore } from 'pinia';
import { store } from '/@/store';
import { ReqParams } from '/@/api/user/model';
import fetchApi from '/@/api/user';
// import { encryptByDES } from '/@/utils/crypto';
import { getToken, setToken, removeToken } from '/@/utils/auth';
import { router } from '/@/router';
interface UserState {
token: string;
auths: string[];
}
export const useUserStore = defineStore({
id: 'app-user',
state: (): UserState => ({
// token
token: '',
// auths
auths: [],
}),
getters: {
getToken(): string {
return this.token || getToken();
},
},
actions: {
setToken(info: string) {
this.token = info ?? ''; // for null or undefined value
setToken(info);
},
setAuth(auths: string[]) {
this.auths = auths;
},
resetState() {
this.token = '';
this.auths = [];
},
/**
* @description: login
*/
async login(params: ReqParams) {
// 密码加密
// params.password = encryptByDES(params.password);
const res = await fetchApi.login(params);
if (res) {
// save token
this.setToken(res.token);
}
return res;
},
/**
* @description: logout
*/
async logout() {
this.resetState();
removeToken();
router.replace('/login');
// 路由表重置
location.reload();
},
},
});
// Need to be used outside the setup
export function useUserStoreWithOut() {
return useUserStore(store);
}

@ -0,0 +1,5 @@
//
// .ant-layout-content{
// }

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

Loading…
Cancel
Save