@ -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', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}); |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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', |
||||||
|
}; |
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 国际化失效问题 |
||||||
|
// 引入dist下的文件:import '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; |
||||||
|
} |
After Width: | Height: | Size: 535 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 822 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 93 KiB |
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{ |
||||||
|
|
||||||
|
// } |