This commit is contained in:
Bunny 2024-11-11 12:04:24 +08:00
commit 89d1e1c33b
497 changed files with 45069 additions and 0 deletions

4
.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

21
.dockerignore Normal file
View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

34
.env Normal file
View File

@ -0,0 +1,34 @@
# 平台本地运行端口号
VITE_PORT=7000
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash"
# 基础请求路径
VITE_BASE_API=/api
# 跨域代理地址
VITE_APP_URL=http://localhost:8801
# mock地址
VITE_MOCK_BASE_API=/mock
# 网络请求延迟时间
VITE_BASE_API_TIMEOUT=60000
# 失败重试次数
VITE_BASE_API_RETRY=5
# 失败重试时间
VITE_BASE_API_RETRY_DELAY=3000
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION="none"
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/

34
.env.development Normal file
View File

@ -0,0 +1,34 @@
# 平台本地运行端口号
VITE_PORT=7000
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash"
# 基础请求路径
VITE_BASE_API=/api
# 跨域代理地址
VITE_APP_URL=http://localhost:7070
# mock地址
VITE_MOCK_BASE_API=/mock
# 网络请求延迟时间
VITE_BASE_API_TIMEOUT=60000
# 失败重试次数
VITE_BASE_API_RETRY=5
# 失败重试时间
VITE_BASE_API_RETRY_DELAY=3000
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION="none"
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/

34
.env.production Normal file
View File

@ -0,0 +1,34 @@
# 平台本地运行端口号
VITE_PORT=80
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash"
# 基础请求路径
VITE_BASE_API=/admin
# 跨域代理地址
VITE_APP_URL=http://localhost:8000
# mock地址
VITE_MOCK_BASE_API=/mock
# 网络请求延迟时间
VITE_BASE_API_TIMEOUT=60000
# 失败重试次数
VITE_BASE_API_RETRY=5
# 失败重试时间
VITE_BASE_API_RETRY_DELAY=3000
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION="none"
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
vite.config.*.timestamp*
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged

20
.lintstagedrc Normal file
View File

@ -0,0 +1,20 @@
{
"*.{js,jsx,ts,tsx}": [
"prettier --cache --ignore-unknown --write",
"eslint --cache --fix"
],
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [
"prettier --cache --write--parser json"
],
"package.json": ["prettier --cache --write"],
"*.vue": [
"prettier --write",
"eslint --cache --fix",
"stylelint --fix --allow-empty-input"
],
"*.{css,scss,html}": [
"prettier --cache --ignore-unknown --write",
"stylelint --fix --allow-empty-input"
],
"*.md": ["prettier --cache --ignore-unknown --write"]
}

11
.markdownlint.json Normal file
View File

@ -0,0 +1,11 @@
{
"default": true,
"MD003": false,
"MD033": false,
"MD013": false,
"MD001": false,
"MD025": false,
"MD024": false,
"MD007": { "indent": 4 },
"no-hard-tabs": false
}

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
shell-emulator=true
shamefully-hoist=true
enable-pre-post-scripts=false
strict-peer-dependencies=false

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v20.15.0

38
.prettierrc.js Normal file
View File

@ -0,0 +1,38 @@
// @see: https://www.prettier.cn
export default {
// 超过最大值换行
printWidth: 160,
// 缩进字节数
tabWidth: 1,
// 使用制表符而不是空格缩进行
useTabs: true,
// 结尾不用分号(true有false没有)
semi: true,
// 使用单引号(true单引号false双引号)
singleQuote: true,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: 'as-needed',
// 在对象,数组括号与文字之间加空格 "{ foo: bar }"
bracketSpacing: true,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>"默认none
trailingComma: 'all',
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: true,
// (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid省略括号 ,always不省略括号
arrowParens: 'avoid',
// 如果文件顶部已经有一个 doclock这个选项将新建一行注释并打上@format标记。
insertPragma: false,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 默认值。因为使用了一些折行敏感型的渲染器如GitHub comment而按照markdown文本样式进行折行
proseWrap: 'preserve',
// 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值 "strict" - 空格被认为是敏感的 "ignore" - 空格被认为是不敏感的
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: 'auto',
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
vueIndentScriptAndStyle: false, // Vue文件脚本和样式标签缩进
};

4
.stylelintignore Normal file
View File

@ -0,0 +1,4 @@
/dist/*
/public/*
public/*
src/style/reset.scss

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Bunny
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.

943
ReadMe.md Normal file
View File

@ -0,0 +1,943 @@
# 项目预览
不知道为什么图床用的使自己的Gitee就是不显示其它GitHub和Gitea都能显示就Gitee显示不出来
或者把项目clone下来看也可以
**线上地址**
- 正式线上预览地址http://111.229.137.235/#/welcome
- 测试预览地址http://106.15.251.123/#/welcome
- 服务器到期时间12月30日
**Github地址**
- [前端地址](https://github.com/BunnyMaster/bunny-admin-web.git)
- [后端地址](https://github.com/BunnyMaster/bunny-admin-server)
**Gitee地址**
- [前端地址](https://gitee.com/BunnyBoss/bunny-admin-web)
- [后端地址](https://gitee.com/BunnyBoss/bunny-admin-server)
## 环境搭建
### 安装docker内容
如果使用是centos或者是rocky
```shell
# 更新yum 和 dnf
yum update -y
dnf update -y
# 安装必要依赖
yum install -y yum-utils device-mapper-persistent-data lvm2
# 设置镜像源
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum list docker-ce --showduplicates | sort -r
# 安装docker
yum -y install docker-ce.x86_64
# 开机启动docker
systemctl enable docker
systemctl start docker
```
### 安装Redis
#### 编写配置文件
```sh
mkdir /bunny/docker_data/my_redis/ -p
vim /bunny/docker_data/my_redis/redis.conf
```
**添加以下内容**
有注释大概率启动不了
```
# bind 127.0.0.1 #注释掉这部分使redis可以外部访问
daemonize no #用守护线程的方式启动
requirepass 123456
appendonly yes #redis持久化  默认是no
tcp-keepalive 300 #防止出现远程主机强迫关闭了一个现有的连接的错误 默认是300
```
**删除注释**
```
daemonize no
requirepass 123456
appendonly yes
tcp-keepalive 300
```
#### 启动Redis
```sh
docker pull redis:7.0.10
docker run -p 6379:6379 --name redis_master \
-v /bunny/docker_data/redis_master/redis.conf:/etc/redis/redis.conf \
-v/bunny/docker_data/redis_master/data:/data \
--restart=always -d redis:7.0.10 --appendonly yes
```
### 安装Minio
```sh
docker run -d \
-p 9000:9000 \
-p 9090:9090 \
--name minio_master --restart=always \
-v /bunny/docker/minio/data:/data \
-e "MINIO_ROOT_USER=bunny" \
-e "MINIO_ROOT_PASSWORD=02120212" \
minio/minio server /data --console-address ":9090"
```
### 安装MySQL
**设置开机启动**
**执行启动3306**
```sh
docker stop master
docker rm master
docker run --name master -p 3306:3306 \
-v /bunny/docker_data/mysql/master/etc/my.cnf:/etc/my.cnf \
-v /bunny/docker_data/mysql/master/data:/var/lib/mysql \
--restart=always --privileged=true \
-e MYSQL_ROOT_PASSWORD=02120212 \
-e TZ=Asia/Shanghai \
mysql:8.0.33 --lower-case-table-names=1
```
**执行启动3304**
其中有创建备份目录
```shell
docker stop slave_3304
docker rm slave_3304
docker run --name slave_3304 -p 3304:3306 \
-v /bunny/docker_data/mysql/slave_3304/etc/my.cnf:/etc/my.cnf \
-v /bunny/docker_data/mysql/slave_3304/data:/var/lib/mysql \
-v /bunny/docker_data/mysql/slave_3304/backup:\
--restart=always --privileged=true \
-e MYSQL_ROOT_PASSWORD=02120212 \
-e TZ=Asia/Shanghai \
mysql:8.0.33 --lower-case-table-names=1
```
**修改密码:**
```sh
docker exec -it mysql_master /bin/bash
mysql -uroot -p02120212
use mysql
ALTER USER 'root'@'%' IDENTIFIED BY "02120212";
FLUSH PRIVILEGES;
```
> my.cnf 配置
>
> ```sql
> [mysqld]
> skip-host-cache
> skip-name-resolve
> secure-file-priv=/var/lib/mysql-files
> user=mysql
>
> # 设置字符集
> character-set-server=utf8mb4
> collation-server=utf8mb4_unicode_ci
>
> # 设置服务器ID如果是复制集群确保每个节点的ID唯一
> server-id=1
>
> # 启用二进制日志
> log-bin=mysql-bin
>
> # 设置表名不区分大小写
> lower_case_table_names = 1
>
> ```
### 数据库文件
在后端文件的根目录中
![image-20241107133345299](http://116.196.101.14:9000/docs/image-20241107133345299.png)
# 项目特点
### 按钮权限显示
如果当前用户在这个页面中只有【添加】和【删除】那么页面按钮中只会显示出【添加按钮】和【删除按钮】
### 去除前后空格
后端配置了自动去除前端传递的空字符串,如果传递的内容前后有空格会自动去除前后的空格
![image-20241105215241811](http://116.196.101.14:9000/docs/image-20241105215241811.png)
代码内容
```java
@ControllerAdvice
public class ControllerStringParamTrimConfig {
/**
* 创建 String trim 编辑器
* 构造方法中 boolean 参数含义为如果是空白字符串,是否转换为null
* 即如果为true,那么 " " 会被转换为 null,否者为 ""
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
StringTrimmerEditor propertyEditor = new StringTrimmerEditor(false);
// 为 String 类对象注册编辑器
binder.registerCustomEditor(String.class, propertyEditor);
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> {
// 为 String 类型自定义反序列化操作
jacksonObjectMapperBuilder
.deserializerByType(String.class, new StdScalarDeserializer<String>(String.class) {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
// 去除全部空格
// return StringUtils.trimAllWhitespace(jsonParser.getValueAsString());
// 仅去除前后空格
return jsonParser.getValueAsString().trim();
}
});
};
}
}
```
### 项目接口和页面
接口地址有两个:
1. knife4j
2. swagger
接口地址://localhost:7070/doc.html#/home
![image-20241105213953503](http://116.196.101.14:9000/docs/image-20241105213953503.png)
swagger接口地址http://localhost:7070/swagger-ui/index.html
![image-20241105214100720](http://116.196.101.14:9000/docs/image-20241105214100720.png)
前端接口地址http://localhost:7000/#/welcome
![image-20241105214230389](http://116.196.101.14:9000/docs/image-20241105214230389.png)
## 登录功能
可以选择邮箱登录或者是密码直接登录,两者不互用。
### 账号登录
![image-20241105212146456](http://116.196.101.14:9000/docs/image-20241105212146456.png)
#### 业务需求
- 用户输入用户名和密码进行登录
#### 实现思路
- 用户输入账号和密码和数据库中账号密码进行比对,成功后进行页面跳转
- 如果账户禁用会显示账户已封禁
**后端实现文件位置**
- 拦截请求为`/admin/login`的请求之后进行登录验证的判断
![image-20241105212722043](http://116.196.101.14:9000/docs/image-20241105212722043.png)
### 邮箱登录
![image-20241105212255972](http://116.196.101.14:9000/docs/image-20241105212255972.png)
#### 业务需求
- 用户输入邮箱账号、密码、邮箱验证码之后进行登录
#### 实现思路
- 需要验证用户输入的邮箱格式是否正确。
- 在未输入验证码的情况下输入密码会提示用户同时后端也会进行验证。如果输入了邮箱验证码但是Redis中不存在或已过期会提示邮箱验证码不存在或已过期。
- 之后对邮箱账号和密码进行判断包括邮箱验证码进行判断
- 判断逻辑如下,文件位置如上图所示。
```java
/**
* * 自定义验证
* 判断邮箱验证码是否正确
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
ObjectMapper objectMapper = new ObjectMapper();
try {
loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
// type不能为空
String type = loginDto.getType();
if (!StringUtils.hasText(type)) {
out(response, Result.error(ResultCodeEnum.REQUEST_IS_EMPTY));
return null;
}
String emailCode = loginDto.getEmailCode();
String username = loginDto.getUsername();
String password = loginDto.getPassword();
// 如果有邮箱验证码,表示是邮箱登录
if (type.equals("email")) {
emailCode = emailCode.toLowerCase();
Object redisEmailCode = redisTemplate.opsForValue().get(RedisUserConstant.getAdminUserEmailCodePrefix(username));
if (redisEmailCode == null) {
out(response, Result.error(ResultCodeEnum.EMAIL_CODE_EMPTY));
return null;
}
// 判断用户邮箱验证码是否和Redis中发送的验证码
if (!emailCode.equals(redisEmailCode.toString().toLowerCase())) {
out(response, Result.error(ResultCodeEnum.EMAIL_CODE_NOT_MATCHING));
return null;
}
}
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
out(response, Result.error(ResultCodeEnum.ILLEGAL_DATA_REQUEST));
return null;
}
}
```
## 首页功能
![image-20241105212403630](http://116.196.101.14:9000/docs/image-20241105212403630.png)
功能菜单,首页图表展示部分功能已经由这个模板作者设计好,其中需要注意的是,如果要查看历史消息或者是进入消息页面可以双击![image-20241105213346408](http://116.196.101.14:9000/docs/image-20241105213346408.png)既可进入消息页面
### 消息功能
![image-20241105213539594](http://116.196.101.14:9000/docs/image-20241105213539594-1730813844820-2.png)
#### 业务需求
1. 消息页面的展示,包含删除、批量删除、选中已读和当前页面所有消息都标为已读
2. 当用户对左侧菜单点击时可以过滤出消息内容,展示不同的消息类型
![image-20241105213720011](http://116.196.101.14:9000/docs/image-20241105213720011.png)
3. 可以点击已读和全部进行筛选消息
![image-20241105214342220](http://116.196.101.14:9000/docs/image-20241105214342220.png)
3. 可以根据标题进行搜搜
4. 包含分页
#### 实现思路
1. 显示当前消息类型,用户点击时带参数请求,只带当前消息类型,不默认携带已读状态查询,然后从数据库筛选并返回结果。
2. 点击"已读"选项时,若选择"全部"之前是设置为undefined这样就不会携带参数了但是底下会有警告现在改为空字符串后端只需过滤掉空字符串即可。
3. 删除选定数据若用户选择列表并筛选出所有ID将数据传递给后端用户删除为逻辑删除
4. 全部标为已读![image-20241106131949217](http://116.196.101.14:9000/docs/image-20241106131949217.png)类似删除操作筛选出选中数据的ID然后传递给后端以标记为已读。
5. 将所有数据标记为已读当前页面前端使用map提取所有ID整合成ID列表传递给后端表示页面上所有数据已读。
6. 输入标题后,随输入变化进行搜索。
**后端代码位置**
![image-20241105213922824](http://116.196.101.14:9000/docs/image-20241105213922824.png)
### 用户管理
![image-20241106002713514](http://116.196.101.14:9000/docs/image-20241106002713514.png)
#### 需求分析
1. 用户操作需要包含CURD的操作
2. 为了方便在用户中需要给出快速禁用当前用户按钮
3. 需要显示用户头像、性别、最后登录的IP地址和归属地
4. 在左侧中需要包含部分查询
5. 可以根据点击的部门查询当前部门下的用户
6. 根据用户可以强制下线、管理员可以修改用户密码、为用户分配角色
![image-20241106002908657](http://116.196.101.14:9000/docs/image-20241106002908657.png)
#### 实现思路
**上传头像**
前端需要剪裁图片内容进行上传后端将前端上传的头像存储到Minio中在上传头像中可以有几菜单可以看到功能菜单。
![image-20241106003056116](http://116.196.101.14:9000/docs/image-20241106003056116.png)
右击时可以看到功能菜单,如上传、下载等功能
![image-20241106003154056](http://116.196.101.14:9000/docs/image-20241106003154056.png)
**重置密码**
重置密码需要判断当前用户密码是否是符合指定的密码格式,并且会根据当前输入密码计算得分如果当前密码复杂则得分越高那么密码强度越强
![image-20241106003256994](http://116.196.101.14:9000/docs/image-20241106003256994.png)
重置密码组件在前端的公共组件文件中
![image-20241106003426573](http://116.196.101.14:9000/docs/image-20241106003426573.png)
**分配角色**
- 给用户分配了admin角色后其他路由绑定和权限设置就不再需要了因为后端会根据admin角色在前端用户信息中设置通用权限码如`*`、`*::*`、`*::*::*`,表示前端用户可以访问所有权限并查看所有内容。
- 管理员有权对用户进行角色分配,这涉及到许多操作,包括菜单显示和接口访问权限。角色与权限相关联,角色也与菜单相关联。
- 当用户访问菜单时,会根据其角色看到其所属的菜单内容。随后,角色与权限接口相关联,根据用户的权限来决定是否显示操作按钮。后端会根据用户的权限验证其是否可以访问当前接口。
- 用户登录或刷新页面时会重新获取用户信息,用户信息中包含角色和权限信息。利用角色和权限信息与前端传递的路径进行比对判断,如果用户包含菜单角色,则可以访问。如果用户包含前端路由中的权限,则表示该权限可以访问。后端也会进行权限判断,以防止通过接口文档等方式访问。
- 分配好角色后,菜单会根据当前路由角色匹配用户角色,从而根据用户角色显示相应的菜单内容。
![image-20241106004533031](http://116.196.101.14:9000/docs/image-20241106004533031.png)
### 角色管理
角色管理包含CURD和权限分配操作
![image-20241106132548236](http://116.196.101.14:9000/docs/image-20241106132548236.png)
#### 业务需求
用户对角色进行CURD操作点击权限设置时让用户分配权限
#### 实现思路
1. 在设计的表中,如果存在相同的角色码,系统会提示用户当前角色已经存在。
![image-20241106132938024](http://116.196.101.14:9000/docs/image-20241106132938024.png)
2. 后端会根据角色的ID分配权限的ID列表。
![image-20241106135600255](http://116.196.101.14:9000/docs/image-20241106135600255.png)
3. 后端在角色权限表中会根据角色的ID分配权限内容。在角色权限表中会先删除当前角色所有的权限内容然后再进行权限内容的重新分配。
```java
public void assignPowersToRole(AssignPowersToRoleDto dto) {
List<Long> powerIds = dto.getPowerIds();
Long roleId = dto.getRoleId();
// 删除这个角色下所有权限
baseMapper.deleteBatchRoleIdsWithPhysics(List.of(roleId));
// 保存分配数据
List<RolePower> rolePowerList = powerIds.stream().map(powerId -> {
RolePower rolePower = new RolePower();
rolePower.setRoleId(roleId);
rolePower.setPowerId(powerId);
return rolePower;
}).toList();
saveBatch(rolePowerList);
// 找到所有和当前更新角色相同的用户
List<Long> roleIds = userRoleMapper.selectList(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getRoleId, roleId))
.stream().map(UserRole::getUserId).toList();
// 根据Id查找所有用户
List<AdminUser> adminUsers = userMapper.selectList(Wrappers.<AdminUser>lambdaQuery().in(!roleIds.isEmpty(), AdminUser::getId, roleIds));
// 用户为空时不更新Redis的key
if (adminUsers.isEmpty()) return;
// 更新Redis中用户信息
List<Long> userIds = adminUsers.stream().map(AdminUser::getId).toList();
roleFactory.updateUserRedisInfo(userIds);
}
```
### 权限管理
![image-20241106135954104](http://116.196.101.14:9000/docs/image-20241106135954104.png)
![image-20241106140006176](http://116.196.101.14:9000/docs/image-20241106140006176.png)
在权限配置中,添加/修改权限时的请求地址为后端接口的请求地址,请求地址使用了【`正则表达式`】判断和【`antpath`】方式填写
> ### 正则表达式
>
> #### 作用和用法:
>
> - **作用**:正则表达式用于描述字符串的特征,可以用来匹配、查找、替换等字符串操作。
> - **用法**在Java中可以使用`java.util.regex`包来支持正则表达式的使用。例如,可以使用`Pattern`和`Matcher`类来编译和匹配正则表达式。
>
> #### 示例:
>
> ```java
> // 匹配邮箱地址的正则表达式示例
> String emailRegex = "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b";
> String email = "example@email.com";
>
> Pattern pattern = Pattern.compile(emailRegex);
> Matcher matcher = pattern.matcher(email);
>
> if (matcher.find()) {
> System.out.println("Valid email address");
> } else {
> System.out.println("Invalid email address");
> }
> ```
>
> ### Ant Path
>
> #### 作用和用法:
>
> - **作用**Ant Path是Spring框架中用来匹配URL路径的一种模式匹配方式类似于Unix系统中的路径匹配规则。
> - **用法**在Spring中Ant Path可以用来匹配URL路径例如在配置Spring的URL映射时可以使用Ant Path来指定匹配规则。
>
> #### 示例:
>
> ```java
> // Ant Path示例
> String pattern = "/users/*/profile";
> String path = "/users/123/profile";
>
> AntPathMatcher matcher = new AntPathMatcher();
> if (matcher.match(pattern, path)) {
> System.out.println("Pattern matched!");
> } else {
> System.out.println("Pattern not matched!");
> }
> ```
>
> Ant Path中支持一些通配符例如`*`匹配任意字符(除了路径分隔符),`**`匹配任意字符包括路径分隔符。Ant Path是一种方便的路径匹配方式可以用来匹配URL路径、文件路径等。
#### 业务需求
1. 对权限表进行CURD操作
2. 在表格中点击新增时父级id为当前点击行的id
#### 实现思路
点击当前行父级id为当前的行的id
![image-20241106140420845](http://116.196.101.14:9000/docs/image-20241106140420845.png)
#### 权限判断实现方式
##### 后端判断方式
判断权限是否可以访问,后端实现判断逻辑
![image-20241106003921315](http://116.196.101.14:9000/docs/image-20241106003921315.png)
##### 前端判断方式
角色分配方式有下面几种想洗参考https://pure-admin.github.io/vue-pure-admin/#/permission/button/router[文档页面](https://pure-admin.github.io/pure-admin-doc/pages/routerMenu/#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B7%AF%E7%94%B1%E7%9A%84-name-%E5%BF%85%E5%86%99-%E8%80%8C%E4%B8%94%E5%BF%85%E9%A1%BB%E5%94%AF%E4%B8%80)
1. 使用标签方式
![image-20241106004247600](http://116.196.101.14:9000/docs/image-20241106004247600.png)
2. 使用函数方式
![image-20241106004310635](http://116.196.101.14:9000/docs/image-20241106004310635.png)
3. 使用指令方式
![image-20241106005252328](http://116.196.101.14:9000/docs/image-20241106005252328.png)
在前端utils文件夹下有`auth.ts`文件里面包含了权限码信息,如果当前菜单属性中包含这个权限码表示可以访问这个权限
![image-20241106004433489](http://116.196.101.14:9000/docs/image-20241106004433489.png)
![image-20241106004500855](http://116.196.101.14:9000/docs/image-20241106004500855.png)
### 菜单管理
![image-20241106140545328](http://116.196.101.14:9000/docs/image-20241106140545328.png)
### 菜单路由
在做菜单返回时必须要了解角色和权限表
![image-20241105213516679](http://116.196.101.14:9000/docs/image-20241105213516679.png)
#### 需求分析
1. 从数据库中返回出所有的菜单,其中需要整合成前端所要的形式,需要包含`roles`和`auths`,及其其它参数。
2. 用户需要根据自己的角色访问不同的菜单。
3. 如果当前用户不可以访问某些按钮需要隐藏。
4. 用户通过其它手段访问如swagger、knife4j、apifox、postman这种工具访问需要做权限验证如果当前用户不满足访问这些接口后端需要拒绝。
5. 如果已经添加了菜单名称、路由等级、路由路径会提示`xxx已存在`![image-20241106132818902](http://116.196.101.14:9000/docs/image-20241106132818902.png)
6. 在数据库中为部分字段建立了唯一索引
![image-20241106132908309](http://116.196.101.14:9000/docs/image-20241106132908309.png)
#### 实现思路
1. 角色和权限哪些可以访问的页面交给前端,在模板中已经设计好,如果用户访问了自己看不到的菜单会出现`403`页面;判断方式是根据后端返回的菜单中如果包含当前用户的角色就表示可以访问当前的菜单,如果用户信息中没有这个角色则表示不可以访问这个页面。
2. 页面是否可以访问只是在操作上,如果用户通过接口访问是阻止不了的,所以这时后端需要在后端中进行判断,当前的访问路径是否是被允许的,也就是这个用户是否有这个权限,权限表设计中包含了请求路径
3. 后端需要判断用户请求这个接口是否有权访问
> 整合成前端格式返回需要递归,后端根据当前用户访问的菜单需要进行递归菜单数据之后返回前端,并将这些菜单绑定的角色放置在`roles`中,之后根据角色查询全新啊相关内容,要将权限内容放置在`auths`中.
>
> 如果包含子菜单需要防止在`children`数组中,后端实现时如果没有子菜单默认是空数组而不是`null`
>
> 大致如下:
>
> ```json
> {
> "menuType": 0,
> "title": "admin_user",
> "path": "/system/admin-user",
> "component": "/system/adminUser/index",
> "meta": {
> "icon": "ic:round-manage-accounts",
> "title": "admin_user",
> "rank": 2,
> "roles": [
> "admin",
> "all_page",
> "system",
> "test"
> ],
> "auths": [
> "message::updateMessage",
> "menuIcon::getMenuIconList",
> "admin::messageReceived",
> "config::getWebConfig",
> "admin::config",
> "i18n::getI18n",
> ....
> ],
> "frameSrc": ""
> },
> "children": [],
> "id": "1841803086252548097",
> "parentId": "1",
> "name": "admin_user",
> "rank": 2
> }
> ```
### 部门管理
![image-20241106140738517](http://116.196.101.14:9000/docs/image-20241106140738517.png)
![image-20241106140728748](http://116.196.101.14:9000/docs/image-20241106140728748.png)
#### 业务需求
1. 包含CURD
2. 在用户管理中可以选择对应的部门
#### 实现思路
1. CURD接口文件如下
![image-20241106140826034](http://116.196.101.14:9000/docs/image-20241106140826034.png)
2. 管理员为用户分配部门
![image-20241106140942278](http://116.196.101.14:9000/docs/image-20241106140942278.png)
### 菜单图标
![image-20241106141037894](http://116.196.101.14:9000/docs/image-20241106141037894.png)
![image-20241106141102601](http://116.196.101.14:9000/docs/image-20241106141102601.png)
#### 业务需求
1. 用户在菜单中可以选择存储在数据库中的图标内容
2. 包含CURD内容
#### 实现思路
后端需要返回接口格式实体类如下
```java
public class MenuIconVo extends BaseUserVo {
@Schema(name = "iconCode", title = "icon类名")
private String iconCode;
@Schema(name = "iconName", title = "icon 名称")
private String iconName;
}
```
![image-20241106141521051](http://116.196.101.14:9000/docs/image-20241106141521051.png)
前端封装好的组件
![image-20241106141626697](http://116.196.101.14:9000/docs/image-20241106141626697.png)
### 邮箱相关
#### 业务需求
1. 邮件用户配置CURD
2. 邮件模板CURD
3. 邮件用户中只能有一个是默认的,如果当前修改其它项需要将其它已经启用改为不启用
4. 邮件模板需要绑定邮件用户
### 实现思路
邮件模板中添加或者修改时前端需要返回所有的邮件模板用户添加或者修改时将用户ID存储在邮件模板的数据字段中
![image-20241106141920350](http://116.196.101.14:9000/docs/image-20241106141920350.png)
### web配置
![image-20241106142001190](http://116.196.101.14:9000/docs/image-20241106142001190.png)
### 系统监控
#### 服务监控
从SpringBoot的Actuator中获取信息页面采用响应式
![image-20241106142208794](http://116.196.101.14:9000/docs/image-20241106142208794.png)
#### 系统缓存
当前内容被SpringBoot缓存会显示在这
![image-20241106142253759](http://116.196.101.14:9000/docs/image-20241106142253759.png)
### 定时任务
采用Quarter持久化存储所有的可以使用的定时任务都在这
![image-20241106142429924](http://116.196.101.14:9000/docs/image-20241106142429924.png)
#### 页面展示
![image-20241106142449033](http://116.196.101.14:9000/docs/image-20241106142449033.png)
![](http://116.196.101.14:9000/docs/image-20241106142449033-1730874298898-1.png)
### 多语言管理
![image-20241106142531047](http://116.196.101.14:9000/docs/image-20241106142531047.png)
![image-20241106142544172](http://116.196.101.14:9000/docs/image-20241106142544172.png)
### 日志管理
![image-20241106142606017](http://116.196.101.14:9000/docs/image-20241106142606017.png)
![image-20241106142614917](http://116.196.101.14:9000/docs/image-20241106142614917.png)
### 消息管理
管理员可以发送消息告诉xxx用户在主页中会显示![image-20241106142908363](http://116.196.101.14:9000/docs/image-20241106142908363.png)
之后点击时会看到消息封面、标题、简介、消息等级、消息等级内容
![image-20241106142949366](http://116.196.101.14:9000/docs/image-20241106142949366.png)
#### 消息类型
![image-20241106143008098](http://116.196.101.14:9000/docs/image-20241106143008098.png)
包含CURD用户编辑消息发送时可以在选择
![image-20241106144017015](http://116.196.101.14:9000/docs/image-20241106144017015.png)
同时在用户消息栏中也会显示对应内容
![image-20241106144050996](http://116.196.101.14:9000/docs/image-20241106144050996.png)
前端判断逻辑如下
![image-20241106144146081](http://116.196.101.14:9000/docs/image-20241106144146081.png)
#### 消息编辑
提供md编辑器和富文本编辑器
![image-20241106144223976](http://116.196.101.14:9000/docs/image-20241106144223976.png)
![image-20241106144246068](http://116.196.101.14:9000/docs/image-20241106144246068.png)
消息接受用户,如果不填写表示全部的用户,填写后会根据填写的内容存储在用户接受表中![image-20241106144522442](http://116.196.101.14:9000/docs/image-20241106144522442.png)
![image-20241106144449463](http://116.196.101.14:9000/docs/image-20241106144449463.png)
消息等级是显示消息样式颜色,文字内容为消息简介内容
![image-20241106144407453](http://116.196.101.14:9000/docs/image-20241106144407453.png)
#### 消息接收管理
根据上面所选的接受用户会出现在下面的用户接受表中,可以对当前用户是否已读进行修改
![image-20241106144307885](http://116.196.101.14:9000/docs/image-20241106144307885.png)
#### 消息发送管理
之前编辑过的消息都会在这
![image-20241106144317746](http://116.196.101.14:9000/docs/image-20241106144317746.png)
# 环境部署
使用Docker进行部署后端接口地址以`/admin`开头,但前端默认请求前缀为`/api`,因此在请求时需要进行替换。详细内容请参考以下【项目部署】说明。
## 配置相关
### docker文件
```dockerfile
# 使用官方的 Nginx 镜像作为基础镜像
FROM nginx
# 删除默认的 Nginx 配置文件
RUN rm /etc/nginx/conf.d/default.conf
# 将自定义的 Nginx 配置文件复制到容器中
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 设置时区,构建镜像时执行的命令
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
# 创建一个目录来存放前端项目文件
WORKDIR /usr/share/nginx/html
# 将前端项目打包文件复制到 Nginx 的默认静态文件目录
COPY dist/ /usr/share/nginx/html
# 复制到nginx目录下
COPY dist/ /etc/nginx/html
# 暴露 Nginx 的默认端口
EXPOSE 80
# 自动启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
```
### NGINX文件
在请求中会使用代理所以会拿不到用户真实的IP地址素以在要NGINX侠做下面的配置这样用户在访问时就可以拿到真实的IP了
```nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
```
#### 如果需要使用https协议
```dockerfile
COPY bunny-web.site.csr /etc/nginx/bunny-web.site.csr
COPY bunny-web.site.key /etc/nginx/bunny-web.site.key
COPY bunny-web.site_bundle.crt /etc/nginx/bunny-web.site_bundle.crt
COPY bunny-web.site_bundle.pem /etc/nginx/bunny-web.site_bundle.pem
```
NGINX的文件
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /etc/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
# 后端跨域请求
location ~/admin/ {
proxy_pass http://172.17.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 404.html;
location = /50x.html {
root html;
}
}
```
## 项目部署
使用WebStorm进行项目部署项目上线时默认端口为80。因此Docker默认暴露的IP端口也应为80NGINX中默认暴露的端口也是80三者应一一对应。
若无法下载请先使用pnpm下载。若不需使用pnpm请删除或修改相应内容。
![image-20241026025057129](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026025057129.png)
### docker配置
![image-20241026024116090](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026024116090.png)
### 配置环境
设置启动端口号和项目地址机器后端请求地址
![image-20241026024813858](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026024813858.png)
#### 配置线上环境
设置项目启动端口号,线上环境默认请求路径为`/admin`需在NGINX中将访问请求前缀更改为`/admin`。
![image-20241026024940747](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026024940747.png)
![image-20241026024243785](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026024243785.png)
#### 配置开发环境
开发环境默认IP为7000若与本地项目端口冲突请修改。后端请求地址为7070。
前端设置的请求前缀为`/api`,但后端接受的前缀为`/admin`,因此需在服务中修改此内容。
![image-20241026024318644](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026024318644.png)
**修改请求路径**
![image-20241026031651591](http://116.196.101.14:9000/docs/auth/undefinedimage-20241026031651591.png)
### 部署命令
```bash
docker build -f Dockerfile -t bunny_auth_web:1.0.0 . && docker run -p 80:80 --name bunny_auth_web --restart always bunny_auth_web:1.0.0
```

53
build/buildEnv.ts Normal file
View File

@ -0,0 +1,53 @@
import { pathResolve } from './utils';
import type { BuildOptions } from 'vite';
export const buildEnvironment = () => {
const environment: BuildOptions = {
target: 'es2015',
assetsInlineLimit: 20000,
// 构建输出的目录,默认值为"dist"
outDir: 'docker/dist',
// 用于指定使用的代码压缩工具。在这里minify 被设置为 'terser',表示使用 Terser 进行代码压缩。默认值terser
// esbuild 打包更快,但是不能去除 console.logterser打包慢但能去除 console.log
minify: 'terser',
// 用于配置 Terser 的选项
terserOptions: {
// 用于配置压缩选项
compress: {
drop_console: true, // 是否删除代码中的 console 语句, 默认值false
drop_debugger: true, // 是否删除代码中的 debugger 语句, 默认值false
},
},
// 禁用 gzip 压缩大小报告,可略微减少打包时间
reportCompressedSize: false,
// 用于指定是否生成源映射文件。源映射文件可以帮助调试和定位源代码中的错误。当设置为false时构建过程不会生成源映射文件
sourcemap: false,
// 用于配置 CommonJS 模块的选项
commonjsOptions: {
// 用于指定是否忽略 CommonJS 模块中的 try-catch 语句。当设置为false时构建过程会保留 CommonJS 模块中的 try-catch 语句
ignoreTryCatch: false,
},
// 规定触发警告的 chunk 大小, 当某个代码分块的大小超过该限制时Vite 会发出警告
chunkSizeWarningLimit: 2000,
rollupOptions: {
external: [],
input: {
index: pathResolve('../index.html', import.meta.url),
},
// 静态资源分类打包
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
manualChunks: id => {
// 如果是包含在包中则打包成 vendor
if (id.includes('node_modules')) {
return `vendor`;
}
},
},
},
};
return environment;
};

61
build/cdn.ts Normal file
View File

@ -0,0 +1,61 @@
import { Plugin as importToCDN } from 'vite-plugin-cdn-import';
/**
* @description `cdn`使cdn模式 .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* 使jscss文件cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
// prodUrl: 'https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}',
prodUrl: 'https://unpkg.com/{name}@{version}/{path}',
modules: [
{
name: 'vue',
var: 'Vue',
path: 'dist/vue.global.prod.js',
},
{
name: 'vue-router',
var: 'VueRouter',
path: 'dist/vue-router.global.js',
},
{
name: 'vue-i18n',
var: 'VueI18n',
path: 'dist/vue-i18n.global.prod.js',
},
// 项目中没有直接安装vue-demi但是pinia用到了所以需要在引入pinia前引入vue-demihttps://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77
{
name: 'vue-demi',
var: 'VueDemi',
path: 'lib/index.iife.js',
},
{
name: 'pinia',
var: 'Pinia',
path: 'dist/pinia.iife.js',
},
{
name: 'element-plus',
var: 'ElementPlus',
path: 'dist/index.full.js',
css: 'dist/index.css',
},
{
name: 'axios',
var: 'axios',
path: 'dist/axios.min.js',
},
{
name: 'dayjs',
var: 'dayjs',
path: 'dayjs.min.js',
},
{
name: 'echarts',
var: 'echarts',
path: 'dist/echarts.min.js',
},
],
});

63
build/compress.ts Normal file
View File

@ -0,0 +1,63 @@
import type { Plugin } from "vite";
import { isArray } from "@pureadmin/utils";
import compressPlugin from "vite-plugin-compression";
export const configCompressPlugin = (
compress: ViteCompression
): Plugin | Plugin[] => {
if (compress === "none") return null;
const gz = {
// 生成的压缩包后缀
ext: ".gz",
// 体积大于threshold才会被压缩
threshold: 0,
// 默认压缩.js|mjs|json|css|html后缀文件设置成true压缩全部文件
filter: () => true,
// 压缩后是否删除原始文件
deleteOriginFile: false
};
const br = {
ext: ".br",
algorithm: "brotliCompress",
threshold: 0,
filter: () => true,
deleteOriginFile: false
};
const codeList = [
{ k: "gzip", v: gz },
{ k: "brotli", v: br },
{ k: "both", v: [gz, br] }
];
const plugins: Plugin[] = [];
codeList.forEach(item => {
if (compress.includes(item.k)) {
if (compress.includes("clear")) {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
);
});
} else {
plugins.push(
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
);
}
} else {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(compressPlugin(vItem));
});
} else {
plugins.push(compressPlugin(item.v));
}
}
}
});
return plugins;
};

85
build/data.js Normal file
View File

@ -0,0 +1,85 @@
/**
* * 自动创建权限内容
*/
(async function requestPath() {
// 获取基础paths对象
const response = await fetch('http://localhost:7070/v3/api-docs/%E9%BB%98%E8%AE%A4%E8%AF%B7%E6%B1%82%E6%8E%A5%E5%8F%A3', { method: 'GET' });
const json = await response.json();
const paths = json.paths;
// 设置父级id顺序
let id = 1;
// 最后整理的数据内容
const data = {};
// 获取所有键
Object.keys(paths)
.filter(item => !item.includes('noAuth') && !item.includes('noManage'))
.forEach(key => {
const pathKey = paths[key];
const { tags, description } = pathKey[Object.keys(pathKey)[0]];
const tag = tags[0];
// 父级内容为info信息
const path = key.match(/\w+\/\w+/, key)[0];
const info = {
id: 1,
parentId: 0,
powerCode: path.replaceAll('/', '::'),
powerName: tag,
requestUrl: undefined,
};
// 整理子级内容信息
const powerCode = key.replace('/admin/', '').replace(/\/\{.*?\}/g, '');
const item = {
parentId: info.id,
powerCode: powerCode.replaceAll('/', '::'),
powerName: description,
requestUrl: key.replace(/\/{.*/, '/.*'),
};
// 向父级内容添加子级Children内容
if (!data[tag]) {
data[tag] = {
info,
children: [item],
};
}
data[tag].children.push(item);
});
// 便利整理好的参数data
for (const item in data) {
// 先添加父级内容
const info = data[item].info;
info.id = id;
await add(info);
// 遍历子级内容向服务器添加
const children = data[item].children;
for (const item1 of children) {
item1.parentId = id;
await add(item1);
}
// 父级添加后id增加
id++;
}
})();
// 向服务器添加的内容
async function add(data) {
const response = await fetch('http://localhost:7070/admin/power/addPower', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token:
'eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAA_yWLywqFIBQA_-WsE9Sjt2xXuz7jmAYGWfiAIu6_X-HuhmHmhVwtjDDXGB_owN8XjKJH3iMayTuo2afFNffHSIdv-eSOEEMuicqZ2raX0Kx22g4ciRkUyBRpw6yxgq1S0SBXubnPBt8fEjhnWnMAAAA.YwSm-NO_6Kg1k1GRwucIt50Y70FbPHoldsdTPVHK_Y4',
},
body: JSON.stringify(data),
});
const json = await response.json();
console.log(json);
}

62
build/info.ts Normal file
View File

@ -0,0 +1,62 @@
import type { Plugin } from "vite";
import { getPackageSize } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import gradientString from "gradient-string";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const welcomeMessage = (VITE_PORT: number) => {
return gradientString("cyan", "magenta").multiline(
`您好! 欢迎使用 bunny 系列开发模板
访
http://localhost:${VITE_PORT}`
);
};
const boxenOptions: BoxenOptions = {
padding: 0.5,
borderColor: "cyan",
borderStyle: "round"
};
export function viteBuildInfo(VITE_PORT: number): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
buildStart() {
console.log(boxen(welcomeMessage(VITE_PORT), boxenOptions));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(
boxen(
gradientString("cyan", "magenta").multiline(
`🎉 恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`
),
boxenOptions
)
);
}
});
}
}
};
}

34
build/optimize.ts Normal file
View File

@ -0,0 +1,34 @@
/**
* `vite.config.ts` `optimizeDeps.include`
* `vite` include esm node_modules/.vite
* include里vite 使 node_modules/.vite
* 使 src/main.ts include vite node_modules/.vite
*/
const include = [
"qs",
"mitt",
"dayjs",
"axios",
"pinia",
"vue-i18n",
"vue-types",
"js-cookie",
"vue-tippy",
"pinyin-pro",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage"
];
/**
*
* `@iconify-icons/` `exclude` 使
*/
const exclude = [
"@iconify-icons/ep",
"@iconify-icons/ri",
"@pureadmin/theme/dist/browser-utils"
];
export { include, exclude };

61
build/plugins.ts Normal file
View File

@ -0,0 +1,61 @@
import { cdn } from './cdn';
import vue from '@vitejs/plugin-vue';
import { pathResolve } from './utils';
import { viteBuildInfo } from './info';
import svgLoader from 'vite-svg-loader';
import type { PluginOption } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx';
import Inspector from 'vite-plugin-vue-inspector';
import { configCompressPlugin } from './compress';
import removeNoMatch from 'vite-plugin-router-warn';
import { visualizer } from 'rollup-plugin-visualizer';
import removeConsole from 'vite-plugin-remove-console';
import { themePreprocessorPlugin } from '@pureadmin/theme';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import { genScssMultipleScopeVars } from '../src/layout/theme';
// import { vitePluginFakeServer } from 'vite-plugin-fake-server';
export function getPluginsList(VITE_CDN: boolean, VITE_COMPRESSION: ViteCompression, VITE_PORT: number): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
vue(),
// jsx、tsx语法支持
vueJsx(),
VueI18nPlugin({
jitCompilation: false,
include: [pathResolve('../locales/**')],
}),
// 按下Command(⌘)+Shift(⇧)然后点击页面元素会自动打开本地IDE并跳转到对应的代码位置
Inspector(),
viteBuildInfo(VITE_PORT),
/**
* vue-router动态路由警告No match found for location with path
* https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359
* vite-plugin-router-warn只在开发环境下启用vue-router文件并且只在服务启动或重启时运行一次
*/
removeNoMatch(),
// // mock支持
// vitePluginFakeServer({
// logger: false,
// include: 'mock',
// infixName: false,
// enableProd: true,// 线上支持mock
// }),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
extract: true,
},
}),
// svg组件化支持
svgLoader(),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console
removeConsole({ external: ['src/assets/iconfont/iconfont.js'] }),
// 打包分析
lifecycle === 'report' ? visualizer({ open: true, brotliSize: true, filename: 'report.html' }) : (null as any),
];
}

36
build/server.ts Normal file
View File

@ -0,0 +1,36 @@
import { loadEnv, type ServerOptions } from 'vite';
import { root, wrapperEnv } from './utils';
export const serverOptions = (mode: string) => {
const { VITE_PORT, VITE_APP_URL } = wrapperEnv(loadEnv(mode, root));
const options: ServerOptions = {
port: VITE_PORT, // ? 端口号
host: '0.0.0.0',
open: true,
cors: true,
proxy: {
'/api': {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, '/admin'),
},
'/admin': {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/admin/, '/admin'),
},
'/mock': {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/mock/, '/mock'),
},
},
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
warmup: {
clientFiles: ['./index.html', './src/{views,components}/*'],
},
};
return options;
};

103
build/utils.ts Normal file
View File

@ -0,0 +1,103 @@
import dayjs from 'dayjs';
import { readdir, stat } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { formatBytes, sum } from '@pureadmin/utils';
import { dependencies, devDependencies, engines, name, version } from '../package.json';
/** 启动`node`进程时所在工作目录的绝对路径 */
const root: string = process.cwd();
/**
* @description
* @param dir `build`
* @param metaUrl `url``build``import.meta.url`
*/
const pathResolve = (dir = '.', metaUrl = import.meta.url) => {
// 当前文件目录的绝对路径
const currentFileDir = dirname(fileURLToPath(metaUrl));
// build 目录的绝对路径
const buildDir = resolve(currentFileDir, 'build');
// 解析的绝对路径
const resolvedPath = resolve(currentFileDir, dir);
// 检查解析的绝对路径是否在 build 目录内
if (resolvedPath.startsWith(buildDir)) {
// 在 build 目录内,返回当前文件路径
return fileURLToPath(metaUrl);
}
// 不在 build 目录内,返回解析后的绝对路径
return resolvedPath;
};
/** 设置别名 */
const alias: Record<string, string> = {
'@': pathResolve('../src'),
'@build': pathResolve(),
};
/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */
const __APP_INFO__ = {
pkg: { name, version, engines, dependencies, devDependencies },
lastBuildTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
};
/** 处理环境变量 */
const wrapperEnv = (envConf: Recordable): ViteEnv => {
// 默认值
const ret: ViteEnv = {
VITE_PORT: 8848,
VITE_PUBLIC_PATH: '',
VITE_ROUTER_HISTORY: '',
VITE_APP_URL: '',
VITE_CDN: false,
VITE_HIDE_HOME: 'false',
VITE_COMPRESSION: 'none',
};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, '\n');
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
if (envName === 'VITE_PORT') {
realName = Number(realName);
}
ret[envName] = realName;
if (typeof realName === 'string') {
process.env[envName] = realName;
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
};
const fileListTotal: number[] = [];
/** 获取指定文件夹中所有文件的总大小 */
const getPackageSize = options => {
const { folder = 'dist', callback, format = true } = options;
readdir(folder, (err, files: string[]) => {
if (err) throw err;
let count = 0;
const checkEnd = () => {
++count == files.length && callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
};
files.forEach((item: string) => {
stat(`${folder}/${item}`, async (err, stats) => {
if (err) throw err;
if (stats.isFile()) {
fileListTotal.push(stats.size);
checkEnd();
} else if (stats.isDirectory()) {
getPackageSize({
folder: `${folder}/${item}/`,
callback: checkEnd,
});
}
});
});
files.length === 0 && callback(0);
});
};
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };

98
commitlint.config.js Normal file
View File

@ -0,0 +1,98 @@
// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import("cz-git").UserConfig} */
export default {
ignores: [commit => commit === 'init'],
extends: ['@commitlint/config-conventional'],
rules: {
// @see: https://commitlint.js.org/#/reference-rules
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
['init', 'feat', 'page', 'media', 'completepage', 'fix', 'fixbug', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert', 'wip', 'workflow', 'types', 'release', 'optimize'],
],
},
prompt: {
messages: {
type: '选择你要提交的类型 :',
scope: '选择一个提交范围(可选):',
customScope: '请输入自定义的提交范围 :',
subject: '填写简短精炼的变更描述 :\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixsSelect: '选择关联issue前缀可选:',
customFooterPrefixs: '输入自定义issue前缀 :',
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
confirmCommit: '是否提交或修改commit ?',
},
types: [
{ value: 'init', name: '初始化: ⏳ 初始化项目', emoji: '⏳' },
{ value: 'optimize', name: '优化代码: ♻️ 优化项目代码', emoji: '♻️' },
{ value: 'feat', name: '新增: 🚀 新增功能', emoji: '🚀' },
{ value: 'media', name: '媒体: 🎁 新增媒体资源', emoji: '🎁' },
{ value: 'page', name: '页面: 📄 新增页面', emoji: '📄' },
{ value: 'completepage', name: '完成页面: 🍻 完成页面', emoji: '🍻' },
{ value: 'fixbug', name: 'bug: 🐛 修改bug', emoji: '🐛' },
{ value: 'fix', name: '修复: 🧩 修复缺陷', emoji: '🧩' },
{ value: 'docs', name: '文档: 📚 文档变更', emoji: '📚' },
{
value: 'style',
name: '格式: 🎨 代码格式(不影响功能,例如空格、分号等格式修正)',
emoji: '🎨',
},
{
value: 'refactor',
name: '重构: 〽️ 代码重构(不包括 bug 修复、功能新增)',
emoji: '〽️',
},
{ value: 'perf', name: '性能: ⚡️ 性能优化', emoji: '⚡️' },
{
value: 'test',
name: '测试: ✅ 添加疏漏测试或已有测试改动',
emoji: '✅',
},
{
value: 'chore',
name: '构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)',
emoji: '📦️',
},
{ value: 'ci', name: '集成: 🎡 修改 CI 配置、脚本', emoji: '🎡' },
{ value: 'revert', name: '回退: ⏪️ 回滚 commit', emoji: '⏪️' },
{ value: 'build', name: '打包: 🔨 项目打包发布', emoji: '🔨' },
],
useEmoji: true,
themeColorCode: '',
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: 'bottom',
customScopesAlias: 'custom',
emptyScopesAlias: 'empty',
upperCaseSubject: false,
allowBreakingChanges: ['feat', 'fix'],
breaklineNumber: 100,
breaklineChar: '|',
skipQuestions: [],
issuePrefixs: [{ value: 'closed', name: 'closed: ISSUES has been processed' }],
customIssuePrefixsAlign: 'top',
emptyIssuePrefixsAlias: 'skip',
customIssuePrefixsAlias: 'custom',
allowCustomIssuePrefixs: true,
allowEmptyIssuePrefixs: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: '',
defaultIssues: '',
defaultScope: '',
defaultSubject: '',
},
};

30
docker/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
# 使用官方的 Nginx 镜像作为基础镜像
FROM nginx
# 删除默认的 Nginx 配置文件
RUN rm /etc/nginx/conf.d/default.conf
# 将自定义的 Nginx 配置文件复制到容器中
COPY nginx.conf /etc/nginx/conf.d/default.conf
#COPY bunny-web.site.csr /etc/nginx/bunny-web.site.csr
#COPY bunny-web.site.key /etc/nginx/bunny-web.site.key
#COPY bunny-web.site_bundle.crt /etc/nginx/bunny-web.site_bundle.crt
#COPY bunny-web.site_bundle.pem /etc/nginx/bunny-web.site_bundle.pem
# 设置时区,构建镜像时执行的命令
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
# 创建一个目录来存放前端项目文件
WORKDIR /usr/share/nginx/html
# 将前端项目打包文件复制到 Nginx 的默认静态文件目录
COPY dist/ /usr/share/nginx/html
# 复制到nginx目录下
COPY dist/ /etc/nginx/html
# 暴露 Nginx 的默认端口
EXPOSE 80
# 自动启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

31
docker/nginx.conf Normal file
View File

@ -0,0 +1,31 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 ;
listen [::]:80;
server_name localhost;
location / {
root /etc/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
# 后端跨域请求
location ~/admin/ {
proxy_pass http://172.17.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 404.html;
location = /50x.html {
root html;
}
}

174
eslint.config.js Normal file
View File

@ -0,0 +1,174 @@
import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import * as parserVue from 'vue-eslint-parser';
import configPrettier from 'eslint-config-prettier';
import pluginPrettier from 'eslint-plugin-prettier';
import { defineFlatConfig } from 'eslint-define-config';
import * as parserTypeScript from '@typescript-eslint/parser';
import pluginTypeScript from '@typescript-eslint/eslint-plugin';
export default defineFlatConfig([
{
...js.configs.recommended,
ignores: ['**/.*', 'dist/*', '*.d.ts', 'public/*', 'src/assets/**', 'src/**/iconfont/**'],
languageOptions: {
globals: {
// index.d.ts
RefType: 'readonly',
EmitType: 'readonly',
TargetContext: 'readonly',
ComponentRef: 'readonly',
ElRef: 'readonly',
ForDataType: 'readonly',
AnyFunction: 'readonly',
PropType: 'readonly',
Writable: 'readonly',
Nullable: 'readonly',
NonNullable: 'readonly',
Recordable: 'readonly',
ReadonlyRecordable: 'readonly',
Indexable: 'readonly',
DeepPartial: 'readonly',
Without: 'readonly',
Exclusive: 'readonly',
TimeoutHandle: 'readonly',
IntervalHandle: 'readonly',
Effect: 'readonly',
ChangeEvent: 'readonly',
WheelEvent: 'readonly',
ImportMetaEnv: 'readonly',
Fn: 'readonly',
PromiseFn: 'readonly',
ComponentElRef: 'readonly',
parseInt: 'readonly',
parseFloat: 'readonly',
},
},
plugins: {
prettier: pluginPrettier,
},
rules: {
...configPrettier.rules,
...pluginPrettier.configs.recommended.rules,
'no-debugger': 'off',
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
},
{
files: ['**/*.?([cm])ts', '**/*.?([cm])tsx'],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': pluginTypeScript,
},
rules: {
...pluginTypeScript.configs.strict.rules,
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-as-const': 'warn',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/prefer-literal-enum-member': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
disallowTypeAnnotations: false,
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
},
},
{
files: ['**/*.d.ts'],
rules: {
'eslint-comments/no-unlimited-disable': 'off',
'import/no-duplicates': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
{
files: ['**/*.?([cm])js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: ['**/*.vue'],
languageOptions: {
globals: {
$: 'readonly',
$$: 'readonly',
$computed: 'readonly',
$customRef: 'readonly',
$ref: 'readonly',
$shallowRef: 'readonly',
$toRef: 'readonly',
},
parser: parserVue,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
extraFileExtensions: ['.vue'],
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
},
plugins: {
vue: pluginVue,
},
processor: pluginVue.processors['.vue'],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs['vue3-essential'].rules,
...pluginVue.configs['vue3-recommended'].rules,
'no-undef': 'off',
'no-unused-vars': 'off',
'vue/no-v-html': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-setup-props-reactivity-loss': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'always',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
},
},
]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

86
index.html Normal file
View File

@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
<meta content="webkit" name="renderer" />
<meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" name="viewport" />
<title>bunny-admin</title>
<link href="/favicon.ico" rel="icon" />
<link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" />
<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
<script>
window.process = {};
</script>
</head>
<body>
<div id="app">
<style>
html,
body,
#app {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.loader,
.loader::before,
.loader::after {
width: 2.5em;
height: 2.5em;
border-radius: 50%;
animation: load-animation 1.8s infinite ease-in-out;
animation-fill-mode: both;
}
.loader {
position: relative;
top: 0;
margin: 80px auto;
font-size: 10px;
color: #406eeb;
text-indent: -9999em;
transform: translateZ(0);
transform: translate(-50%, 0);
animation-delay: -0.16s;
}
.loader::before,
.loader::after {
position: absolute;
top: 0;
content: '';
}
.loader::before {
left: -3.5em;
animation-delay: -0.32s;
}
.loader::after {
left: 3.5em;
}
@keyframes load-animation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
<div class="loader"></div>
</div>
<script src="/src/main.ts" type="module"></script>
</body>
</html>

10
lint-staged.config.js Normal file
View File

@ -0,0 +1,10 @@
export default {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
"prettier --write--parser json"
],
"package.json": ["prettier --write"],
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
"*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"],
"*.md": ["prettier --write"]
};

177
package.json Normal file
View File

@ -0,0 +1,177 @@
{
"name": "financial-admin",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"serve": "pnpm vite",
"start": "vite",
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build && generate-version-file",
"build:staging": "rimraf dist && vite build --mode staging",
"report": "rimraf dist && vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f . -r",
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky install",
"preinstall": "npx only-allow pnpm",
"commit": "git pull && git add -A && git-cz && git push"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@howdyjs/mouse-menu": "^2.1.3",
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.4.7",
"@vue-flow/background": "^1.3.0",
"@vue-flow/core": "^1.33.6",
"@vueuse/core": "^10.9.0",
"@vueuse/motion": "^2.1.0",
"@wangeditor/editor-for-vue": "^5.1.12",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.6.8",
"china-area-data": "^5.0.1",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"el-table-infinite-scroll": "^3.0.3",
"element-plus": "2.7.1",
"intro.js": "^7.2.0",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",
"jsbarcode": "^3.11.6",
"localforage": "^1.10.0",
"md-editor-v3": "^4.21.2",
"mint-filter": "^4.0.3",
"mitt": "^3.0.1",
"mqtt": "4.3.7",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"pinyin-pro": "^3.20.4",
"plus-pro-components": "^0.1.1",
"qrcode": "^1.5.3",
"qs": "^6.12.1",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.2",
"swiper": "^11.1.1",
"terser": "^5.31.0",
"typeit": "^8.8.3",
"v-contextmenu": "^3.2.0",
"v3-infinite-loading": "^1.3.1",
"version-rocket": "^1.7.1",
"vite-plugin-vue-inspector": "^5.1.3",
"vue": "^3.4.27",
"vue-i18n": "^9.13.1",
"vue-json-pretty": "^2.4.0",
"vue-pdf-embed": "^2.0.3",
"vue-router": "^4.3.2",
"vue-tippy": "^6.4.1",
"vue-types": "^5.1.2",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue-waterfall-plugin-next": "^2.4.3",
"vue3-danmaku": "^1.6.0",
"vue3-puzzle-vcode": "^1.1.7",
"vuedraggable": "^4.1.0",
"vxe-table": "^4.6.9",
"wavesurfer.js": "^7.7.13",
"xgplayer": "^3.0.17",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3",
"@eslint/js": "^9.2.0",
"@faker-js/faker": "^8.4.1",
"@iconify-icons/ep": "^1.2.12",
"@iconify-icons/ri": "^1.2.10",
"@iconify/vue": "^4.1.2",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@pureadmin/theme": "^3.2.0",
"@types/dagre": "^0.7.52",
"@types/gradient-string": "^1.1.6",
"@types/intro.js": "^5.1.5",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.12.11",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.15",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.19",
"boxen": "^7.1.1",
"commitizen": "^4.2.4",
"commitlint": "^17.0.1",
"cssnano": "^7.0.1",
"cz-git": "^1.3.2",
"dagre": "^0.8.5",
"eslint": "^9.2.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.25.0",
"gradient-string": "^2.0.2",
"husky": "^8.0.1",
"lint-staged": "^15.2.2",
"postcss": "^8.4.38",
"postcss-html": "^1.7.0",
"postcss-import": "^16.1.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.0",
"stylelint": "^16.5.0",
"stylelint-config-recess-order": "^5.0.1",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-prettier": "^5.0.0",
"svgo": "^3.3.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^9.4.2",
"vue-tsc": "^1.8.27"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=8.6.10"
},
"pnpm": {
"allowedDeprecatedVersions": {
"sourcemap-codec": "*",
"domexception": "*",
"w3c-hr-time": "*",
"stable": "*",
"abab": "*"
},
"peerDependencyRules": {
"allowedVersions": {
"eslint": "9"
}
}
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

11002
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

12
postcss.config.js Normal file
View File

@ -0,0 +1,12 @@
// @ts-check
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
public/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

83
src/App.vue Normal file
View File

@ -0,0 +1,83 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts" setup>
import { ElConfigProvider } from 'element-plus';
import { computed, onMounted } from 'vue';
import { ReDialog } from '@/components/BaseDialog';
import en from 'element-plus/es/locale/lang/en';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import plusEn from 'plus-pro-components/es/locale/lang/en';
import plusZhCn from 'plus-pro-components/es/locale/lang/zh-cn';
import { useNav } from '@/layout/hooks/useNav';
import { useI18n } from 'vue-i18n';
import { userI18nStore } from '@/store/i18n/i18n';
const i18nStore = userI18nStore();
const i18n = useI18n();
const { $storage } = useNav();
/**
* * 设置多语言内容
*/
const setI18n = async () => {
await i18nStore.fetchI18n();
const languageData = JSON.parse(localStorage.getItem('i18nStore') as any);
//
const locale = $storage.locale.locale;
//
if (locale == '' || locale == null || !locale) {
const local = languageData.i18n.local;
i18n.locale.value = local;
$storage.locale = { locale: local };
i18n.mergeLocaleMessage(local, languageData.i18n[local]);
return;
}
i18n.locale.value = locale;
$storage.locale = { locale };
i18nStore.i18n.local = locale;
i18n.mergeLocaleMessage(locale, languageData.i18n[locale]);
};
/**
* * 当前语言类别
*/
const currentLocale = computed(() => {
const languageData = JSON.parse(localStorage.getItem('i18nStore') as any);
const local = languageData ? languageData.i18n.local : {};
return local === 'zh' ? { ...zhCn, ...plusZhCn } : { ...plusEn, ...en };
});
onMounted(() => {
//
setI18n();
});
</script>
<style>
/* 定义滚动条高宽及背景高宽分别对应横竖滚动条的尺寸 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: var(--el-text-color-secondary);
}
/* 定义滚动条轨道内阴影+圆角 */
::-webkit-scrollbar-track {
background-color: #ebecef;
border-radius: 5px;
box-shadow: inset 0 0 6px #ebecef;
}
/* 定义滑块内阴影+圆角 */
::-webkit-scrollbar-thumb {
background-color: #d0d2d6;
border-radius: 5px;
box-shadow: inset 0 0 6px #d0d2d6;
}
</style>

41
src/api/service/config.ts Normal file
View File

@ -0,0 +1,41 @@
import type { AxiosRequestConfig, CustomParamsSerializer } from 'axios';
import { stringify } from 'qs';
export const whiteList = ['/refresh-token', '/login'];
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
export const defaultConfig: AxiosRequestConfig = {
// 默认请求地址
baseURL: import.meta.env.VITE_BASE_API,
// 设置超时时间
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
// @ts-expect-error
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
// 跨域允许携带凭证
// withCredentials: true,
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
// 数组格式参数序列化https://github.com/axios/axios/issues/5142
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer,
},
};
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
export const defaultMockConfig: AxiosRequestConfig = {
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
baseURL: import.meta.env.VITE_MOCK_BASE_API || '/mock',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
// 数组格式参数序列化https://github.com/axios/axios/issues/5142
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer,
},
};

View File

@ -0,0 +1,152 @@
import Axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
import type { PureHttpError, PureHttpRequestConfig, PureHttpResponse, RequestMethods } from './types';
import NProgress from '../../utils/progress';
import { formatToken, getToken } from '@/utils/auth';
import { useUserStoreHook } from '@/store/system/user';
import { defaultMockConfig } from '@/api/service/config';
class PureHttp {
/** `token`过期后,暂存待执行的请求 */
private static requests = [];
/** 防止重复刷新`token` */
private static isRefreshing = false;
/** 初始化配置对象 */
private static initConfig: PureHttpRequestConfig = {};
/** 保存当前`Axios`实例对象 */
private static axiosInstance: AxiosInstance = Axios.create(defaultMockConfig);
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
/** 重连原始请求 */
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers['token'] = formatToken(token);
resolve(config);
});
});
}
/** 通用请求工具函数 */
public request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T> {
const config = {
method,
url,
...param,
...axiosConfig,
} as PureHttpRequestConfig;
// 单独处理自定义请求/响应回调
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
}
/** 单独抽离的`post`工具函数 */
public post<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('post', url, params, config);
}
/** 单独抽离的`get`工具函数 */
public get<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('get', url, params, config);
}
/** 请求拦截 */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === 'function') {
config.beforeRequestCallback(config);
return config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ['/refresh-token', '/login'];
return whiteList.some(url => config.url.endsWith(url))
? config
: new Promise(resolve => {
const data: any = getToken();
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then((res: any) => {
const token = res.data.accessToken;
config.headers['Authorization'] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
}
resolve(PureHttp.retryOriginalRequest(config));
} else {
config.headers['Authorization'] = formatToken(data.accessToken);
resolve(config);
}
} else {
resolve(config);
}
});
},
error => {
return Promise.reject(error);
},
);
}
/** 响应拦截 */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === 'function') {
$config.beforeResponseCallback(response);
return response.data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
},
);
}
}
export const http = new PureHttp();

164
src/api/service/request.ts Normal file
View File

@ -0,0 +1,164 @@
import Axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
import type { PureHttpError, PureHttpRequestConfig, PureHttpResponse, RequestMethods } from './types';
import NProgress from '@/utils/progress';
import { formatToken, getToken, removeToken } from '@/utils/auth';
import { useUserStoreHook } from '@/store/system/user';
import { message } from '@/utils/message';
import { router } from '@/store/utils';
import { defaultConfig, whiteList } from '@/api/service/config';
class PureHttp {
/** `token`过期后,暂存待执行的请求 */
private static requests = [];
/** 防止重复刷新`token` */
private static isRefreshing = false;
/** 初始化配置对象 */
private static initConfig: PureHttpRequestConfig = {};
/** 保存当前`Axios`实例对象 */
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
/** 重连原始请求 */
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers['token'] = formatToken(token);
resolve(config);
});
});
}
/** 通用请求工具函数 */
public request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T> {
const config = { method, url, ...param, ...axiosConfig } as PureHttpRequestConfig;
// 单独处理自定义请求/响应回调
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
}
/** 单独抽离的`post`工具函数 */
public post<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('post', url, params, config);
}
/** 单独抽离的`get`工具函数 */
public get<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('get', url, params, config);
}
/** 请求拦截 */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === 'function') {
config.beforeRequestCallback(config);
return config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
return whiteList.some(url => config.url.endsWith(url))
? config
: new Promise(resolve => {
const data = getToken();
// 存在token
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then((res: any) => {
// 从结果中获取token
const token = res.data.token;
config.headers['token'] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
}
resolve(PureHttp.retryOriginalRequest(config));
} else {
config.headers['token'] = formatToken(data.token);
resolve(config);
}
} else {
resolve(config);
}
});
},
error => error,
);
}
/** 响应拦截 */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
async (response: PureHttpResponse) => {
const $config = response.config;
const data = response.data;
// 关闭进度条动画
NProgress.done();
// 登录过期,和异常处理
if (data.code === 208) {
message(data.message, { type: 'warning' });
removeToken();
await router.push('/login');
} else if (data.code >= 201 && data.code < 300) {
message(data.message, { type: 'warning' });
} else if (data.code > 300) {
message(data.message, { type: 'error' });
}
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === 'function') {
$config.beforeResponseCallback(response);
return data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return data;
}
return data;
},
(error: PureHttpError) => {
error.isCancelRequest = Axios.isCancel(error);
// 关闭进度条动画
NProgress.done();
message(error.message, { type: 'error' });
return error;
},
);
}
}
export const http = new PureHttp();

46
src/api/service/types.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
import type { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
// 基础后端返回内容
export interface BaseResult<T> {
code: number;
data: T;
message: string;
}
export interface ResultTable {
/** 列表数据 */
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
pageNo?: number;
}
export type resultType = {
accessToken?: string;
};
export type RequestMethods = Extract<Method, 'get' | 'post' | 'put' | 'delete' | 'patch' | 'option' | 'head'>;
export interface PureHttpError extends AxiosError {
isCancelRequest?: boolean;
}
export interface PureHttpResponse extends AxiosResponse {
config: PureHttpRequestConfig;
}
export interface PureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
beforeResponseCallback?: (response: PureHttpResponse) => void;
}
export default class PureHttp {
request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T>;
post<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T>;
get<T, P>(url: string, params?: P, config?: PureHttpRequestConfig): Promise<T>;
}

36
src/api/v1/actuator.ts Normal file
View File

@ -0,0 +1,36 @@
import { http } from '@/api/service/request';
/** actuator断端点-系统服务获取 */
export const fetchSystemHealthList = () => {
return http.request<any>('get', 'actuator/health');
};
/** actuator断端点-系统信息 */
export const fetchSystemInfo = () => {
return http.request<any>('get', 'actuator/info');
};
/** actuator断端点-系统缓存 */
export const fetchSystemCaches = () => {
return http.request<any>('get', 'actuator/caches');
};
/** actuator断端点-CPU占用 */
export const fetchSystemCPU = () => {
return http.request<any>('get', 'actuator/metrics/system.cpu.usage');
};
/** actuator断端点-CPU占用 */
export const fetchSystemProcessCPU = () => {
return http.request<any>('get', 'actuator/metrics/process.cpu.usage');
};
/** actuator断端点-磁盘可用 */
export const fetchSystemDiskFree = () => {
return http.request<any>('get', 'actuator/metrics/disk.free');
};
/** actuator断端点-磁盘总量 */
export const fetchSystemDiskTotal = () => {
return http.request<any>('get', 'actuator/metrics/disk.total');
};

View File

@ -0,0 +1,12 @@
import { http } from '@/api/service/request';
import type { BaseResult } from '@/api/service/types';
/** 获取修改前端配置文件 */
export const fetchGetWebConfig = () => {
return http.request<BaseResult<any>>('get', '/config/getWebConfig');
};
/** 更新web配置文件 */
export const fetchUpdateWebConfiguration = (data: any) => {
return http.request<BaseResult<any>>('put', '/config/updateWebConfiguration', { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 邮件模板表---获取邮件模板表列表 */
export const fetchGetEmailTemplateList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `emailTemplate/getEmailTemplateList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 邮件模板表---获取模板类型字段 */
export const fetchGetEmailTypes = () => {
return http.request<BaseResult<any>>('get', 'emailTemplate/getEmailTypes');
};
/** 邮件模板表---添加邮件模板表 */
export const fetchAddEmailTemplate = (data: any) => {
return http.request<BaseResult<object>>('post', 'emailTemplate/addEmailTemplate', { data });
};
/** 邮件模板表---更新邮件模板表 */
export const fetchUpdateEmailTemplate = (data: any) => {
return http.request<BaseResult<object>>('put', 'emailTemplate/updateEmailTemplate', { data });
};
/** 邮件模板表---删除邮件模板表 */
export const fetchDeleteEmailTemplate = (data: any) => {
return http.request<BaseResult<object>>('delete', 'emailTemplate/deleteEmailTemplate', { data });
};

View File

@ -0,0 +1,32 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 邮箱用户发送配置管理---获取邮箱用户发送配置管理列表 */
export const fetchGetEmailUsersList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `emailUsers/getEmailUsersList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 邮箱用户发送配置管理---获取所有邮箱配置用户 */
export const fetchGetAllMailboxConfigurationUsers = () => {
return http.request<BaseResult<any>>('get', 'emailUsers/noManage/getAllMailboxConfigurationUsers');
};
/** 邮箱用户发送配置管理---添加邮箱用户发送配置管理 */
export const fetchAddEmailUsers = (data: any) => {
return http.request<BaseResult<object>>('post', 'emailUsers/addEmailUsers', { data });
};
/** 邮箱用户发送配置管理---更新邮箱用户发送配置管理 */
export const fetchUpdateEmailUsers = (data: any) => {
return http.request<BaseResult<object>>('put', 'emailUsers/updateEmailUsers', { data });
};
/** 邮箱用户发送配置管理---更新邮箱用户状态 */
export const fetchUpdateEmailUserStatus = (data: any) => {
return http.request<BaseResult<object>>('put', 'emailUsers/updateEmailUserStatus', { data });
};
/** 邮箱用户发送配置管理---删除邮箱用户发送配置管理 */
export const fetchDeleteEmailUsers = (data: any) => {
return http.request<BaseResult<object>>('delete', 'emailUsers/deleteEmailUsers', { data });
};

42
src/api/v1/files.ts Normal file
View File

@ -0,0 +1,42 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 系统文件管理---获取系统文件管理列表 */
export const fetchGetFilesList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `files/getFilesList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 系统文件管理---根据Id下载系统文件 */
export const downloadFilesByFileId = (data: any) => {
return http.request<any>('get', `files/downloadFilesByFileId/${data.id}`, { responseType: 'blob' });
};
/** 系统文件管理---批量下载系统文件 */
export const downloadFilesByFilepath = (data: any) => {
return http.request<any>('get', `files/downloadFilesByFilepath`, { params: data, responseType: 'blob' });
};
/** 系统文件管理---获取所有文件类型 */
export const fetchGetAllMediaTypes = () => {
return http.request<BaseResult<any>>('get', `files/noManage/getAllMediaTypes`);
};
/** 系统文件管理---获取所有文件存储基础路径 */
export const fetchGetAllFilesStoragePath = () => {
return http.request<BaseResult<any>>('get', `files/noManage/getAllFilesStoragePath`);
};
/** 系统文件管理---添加系统文件管理 */
export const fetchAddFiles = (data: any) => {
return http.request<BaseResult<any>>('post', 'files/addFiles', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/** 系统文件管理---更新系统文件管理 */
export const fetchUpdateFiles = (data: any) => {
return http.request<BaseResult<object>>('put', 'files/updateFiles', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/** 系统文件管理---删除系统文件管理 */
export const fetchDeleteFiles = (data: any) => {
return http.request<BaseResult<any>>('delete', 'files/deleteFiles', { data });
};

View File

@ -0,0 +1,24 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 账单信息---获取账单信息列表 */
export const fetchGetBillList = (data: any) => {
// 删除这个请求参数
delete data.date;
return http.request<BaseResult<ResultTable>>('get', `bill/noManage/getBillList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 账单信息---添加账单信息 */
export const fetchAddBill = (data: any) => {
return http.request<BaseResult<object>>('post', 'bill/noManage/addBill', { data });
};
/** 账单信息---更新账单信息 */
export const fetchUpdateBill = (data: any) => {
return http.request<BaseResult<object>>('put', 'bill/noManage/updateBill', { data });
};
/** 账单信息---删除账单信息 */
export const fetchDeleteBill = (data: any) => {
return http.request<BaseResult<object>>('delete', 'bill/noManage/deleteBill', { data });
};

View File

@ -0,0 +1,22 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 分类信息---获取分类信息列表 */
export const fetchGetCategoryList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `category/getCategoryList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 分类信息---添加分类信息 */
export const fetchAddCategory = (data: any) => {
return http.request<BaseResult<object>>('post', 'category/addCategory', { data });
};
/** 分类信息---更新分类信息 */
export const fetchUpdateCategory = (data: any) => {
return http.request<BaseResult<object>>('put', 'category/updateCategory', { data });
};
/** 分类信息---删除分类信息 */
export const fetchDeleteCategory = (data: any) => {
return http.request<BaseResult<object>>('delete', 'category/deleteCategory', { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 分类信息---获取分类信息列表 */
export const fetchGetCategoryUserList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `categoryUser/noManage/getCategoryUserList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 分类信息---查询当前用户下所有的分类 */
export const fetchGetCategoryUserAllList = () => {
return http.request<BaseResult<object>>('get', 'categoryUser/noManage/getCategoryUserAllList');
};
/** 分类信息---添加分类信息 */
export const fetchAddCategoryUser = (data: any) => {
return http.request<BaseResult<object>>('post', 'categoryUser/noManage/addCategoryUser', { data });
};
/** 分类信息---更新分类信息 */
export const fetchUpdateCategoryUser = (data: any) => {
return http.request<BaseResult<object>>('put', 'categoryUser/noManage/updateCategoryUser', { data });
};
/** 分类信息---删除分类信息 */
export const fetchDeleteCategoryUser = (data: any) => {
return http.request<BaseResult<object>>('delete', 'categoryUser/noManage/deleteCategoryUser', { data });
};

47
src/api/v1/i18n.ts Normal file
View File

@ -0,0 +1,47 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 多语言类型管理---获取多语言内容 */
export const fetchGetI18n = () => {
return http.request<BaseResult<object>>('get', 'i18n/getI18n');
};
/** 多语言类型管理---获取多语言列表 */
export const fetchGetI18nList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `i18n/getI18nList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 多语言类型管理---添加多语言 */
export const fetchAddI18n = (data: any) => {
return http.request<BaseResult<object>>('post', 'i18n/addI18n', { data });
};
/** 多语言类型管理---更新多语言 */
export const fetchUpdateI18n = (data: any) => {
return http.request<BaseResult<object>>('put', 'i18n/updateI18n', { data });
};
/** 多语言类型管理---删除多语言 */
export const fetchDeleteI18n = (data: any) => {
return http.request<BaseResult<object>>('delete', 'i18n/deleteI18n', { data });
};
/** 多语言类型管理---获取多语言类型列表 */
export const fetchGetI18nTypeList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', 'i18nType/noAuth/getI18nTypeList', { params: data });
};
/** 多语言类型管理---添加多语言类型 */
export const fetchAddI18nType = (data: any) => {
return http.request<BaseResult<object>>('post', 'i18nType/addI18nType', { data });
};
/** 多语言类型管理---更新多语言类型 */
export const fetchUpdateI18nType = (data: any) => {
return http.request<BaseResult<object>>('put', 'i18nType/updateI18nType', { data });
};
/** 多语言类型管理---删除多语言类型 */
export const fetchDeleteI18nType = (data: any) => {
return http.request<BaseResult<object>>('delete', 'i18nType/deleteI18nType', { data });
};

View File

@ -0,0 +1,12 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 调度任务执行日志---获取调度任务执行日志列表 */
export const fetchGetQuartzExecuteLogList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `quartzExecuteLog/getQuartzExecuteLogList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 调度任务执行日志---删除调度任务执行日志 */
export const fetchDeleteQuartzExecuteLog = (data: any) => {
return http.request<BaseResult<object>>('delete', 'quartzExecuteLog/deleteQuartzExecuteLog', { data });
};

View File

@ -0,0 +1,17 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 用户登录日志---获取用户登录日志列表 */
export const fetchGetUserLoginLogList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `userLoginLog/getUserLoginLogList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 用户登录日志---获取用户登录日志列表 */
export const fetchGetUserLoginLogListByLocalUser = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `userLoginLog/noManage/getUserLoginLogListByLocalUser/${data.currentPage}/${data.pageSize}`);
};
/** 用户登录日志---删除用户登录日志 */
export const fetchDeleteUserLoginLog = (data: any) => {
return http.request<BaseResult<object>>('delete', 'userLoginLog/deleteUserLoginLog', { data });
};

49
src/api/v1/menu/menu.ts Normal file
View File

@ -0,0 +1,49 @@
import { http } from '@/api/service/request';
import type { BaseResult } from '@/api/service/types';
/** 菜单管理-列表 */
export const fetchGetMenusList = (data?: any) => {
return http.request<BaseResult<any>>('get', `router/getMenusList`, { params: data });
};
/**
* id获取所有角色
*/
export const fetchGetRoleListByRouterId = data => {
return http.request<BaseResult<any>>('get', `routerRole/getRoleListByRouterId`, { params: data });
};
/** 菜单管理-添加菜单 */
export const fetchAddMenu = (data?: any) => {
return http.request<BaseResult<any>>('post', `router/addMenu`, { data });
};
/** 菜单管理-为菜单分配角色 */
export const fetchAssignRolesToRouter = (data: any) => {
return http.request<BaseResult<any>>('post', `routerRole/assignRolesToRouter`, { data });
};
/** 菜单管理-批量为菜单添加角色 */
export const fetchAssignAddBatchRolesToRouter = (data: any) => {
return http.request<BaseResult<any>>('post', `routerRole/assignAddBatchRolesToRouter`, { data });
};
/** 菜单管理-清除选中菜单所有角色 */
export const fetchClearAllRolesSelect = (data: any) => {
return http.request<BaseResult<any>>('delete', `routerRole/clearAllRolesSelect`, { data });
};
/** 菜单管理-更新菜单 */
export const fetchUpdateMenu = (data?: any) => {
return http.request<BaseResult<any>>('put', `router/updateMenu`, { data });
};
/** 菜单管理-快速更新菜单排序 */
export const fetchUpdateMenuByIdWithRank = (data?: any) => {
return http.request<BaseResult<any>>('put', `router/updateMenuByIdWithRank`, { data });
};
/** 菜单管理-删除菜单 */
export const fetchDeletedMenuByIds = (data?: any) => {
return http.request<BaseResult<any>>('delete', `router/deletedMenuByIds`, { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 系统菜单图标---获取多语言列表 */
export const fetchGetMenuIconList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `menuIcon/getMenuIconList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 系统菜单图标---添加多语言 */
export const fetchAddMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('post', 'menuIcon/addMenuIcon', { data });
};
/** 系统菜单图标---更新多语言 */
export const fetchUpdateMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('put', 'menuIcon/updateMenuIcon', { data });
};
/** 系统菜单图标---删除多语言 */
export const fetchDeleteMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('delete', 'menuIcon/deleteMenuIcon', { data });
};
/** 系统菜单图标---根据iconName搜索menuIcon */
export const fetchGetIconNameList = (data: any) => {
return http.request<BaseResult<object>>('get', 'menuIcon/noManage/getIconNameList', { params: data });
};

View File

@ -0,0 +1,17 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 管理员操作用户消息---获取系统管理消息列表 */
export const fetchGetMessageReceivedList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `messageReceived/getMessageReceivedList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 管理员操作用户消息---将用户消息标为已读 */
export const fetchUpdateMarkMessageReceived = (data: any) => {
return http.request<BaseResult<object>>('put', 'messageReceived/updateMarkMessageReceived', { data });
};
/** 管理员操作用户消息---管理删除用户消息 */
export const fetchDeleteMessageReceivedByIds = (data: any) => {
return http.request<BaseResult<object>>('delete', 'messageReceived/deleteMessageReceivedByIds', { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 系统消息---获取系统管理消息列表 */
export const fetchGetMessageList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `message/getMessageList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 系统消息---根据消息id获取接收人信息 */
export const fetchGetReceivedUserinfoByMessageId = (data: any) => {
return http.request<BaseResult<any>>('get', `message/noManage/getReceivedUserinfoByMessageId`, { params: data });
};
/** 系统消息---添加系统消息 */
export const fetchAddMessage = (data: any) => {
return http.request<BaseResult<object>>('post', 'message/addMessage', { data });
};
/** 系统消息---更新系统消息 */
export const fetchUpdateMessage = (data: any) => {
return http.request<BaseResult<object>>('put', 'message/updateMessage', { data });
};
/** 系统消息---删除系统消息 */
export const fetchDeleteMessage = (data: any) => {
return http.request<BaseResult<object>>('delete', 'message/deleteMessage', { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 系统消息类型---获取系统消息类型列表 */
export const fetchGetMessageTypeList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `messageType/getMessageTypeList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 系统消息类型---获取系统消息类型列表 */
export const fetchGetAllMessageTypes = () => {
return http.request<BaseResult<ResultTable>>('get', '/messageType/noManage/getAllMessageTypes');
};
/** 系统消息类型---添加系统消息类型 */
export const fetchAddMessageType = (data: any) => {
return http.request<BaseResult<object>>('post', 'messageType/addMessageType', { data });
};
/** 系统消息类型---更新系统消息类型 */
export const fetchUpdateMessageType = (data: any) => {
return http.request<BaseResult<object>>('put', 'messageType/updateMessageType', { data });
};
/** 系统消息类型---删除系统消息类型 */
export const fetchDeleteMessageType = (data: any) => {
return http.request<BaseResult<object>>('delete', 'messageType/deleteMessageType', { data });
};

View File

@ -0,0 +1,22 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 用户系统消息---用户获取系统消息列表 */
export const fetchGetUserMessageList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `messageReceived/noManage/getUserMessageList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 用户系统消息---根据消息id查询消息详情 */
export const fetchGetMessageDetailById = (data: any) => {
return http.request<BaseResult<any>>('get', `message/noManage/getMessageDetailById`, { params: data });
};
/** 系统消息---用户将消息标为已读 */
export const fetchUpdateUserMarkAsRead = (data: any) => {
return http.request<BaseResult<object>>('put', 'messageReceived/noManage/userMarkAsRead', { data });
};
/** 系统消息---用户删除系统消息 */
export const fetchDeleteUserMessageByIds = (data: any) => {
return http.request<BaseResult<object>>('delete', 'messageReceived/noManage/deleteUserMessageByIds', { data });
};

View File

@ -0,0 +1,37 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** Schedulers视图---获取Schedulers视图列表 */
export const fetchGetSchedulersList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `schedulers/getSchedulersList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** Schedulers视图---获取所有可用调度任务 */
export const fetchGetAllScheduleJobList = () => {
return http.request<BaseResult<ResultTable>>('get', 'schedulers/noManage/getAllScheduleJobList');
};
/** Schedulers视图---添加Schedulers视图 */
export const fetchAddSchedulers = (data: any) => {
return http.request<BaseResult<object>>('post', 'schedulers/addSchedulers', { data });
};
/** Schedulers视图---更新Schedulers视图 */
export const fetchUpdateSchedulers = (data: any) => {
return http.request<BaseResult<object>>('put', 'schedulers/updateSchedulers', { data });
};
/** Schedulers视图---暂停任务 */
export const fetchPauseSchedulers = (data: any) => {
return http.request<BaseResult<object>>('put', 'schedulers/pauseSchedulers', { data });
};
/** Schedulers视图---恢复任务 */
export const fetchResumeSchedulers = (data: any) => {
return http.request<BaseResult<object>>('put', 'schedulers/resumeSchedulers', { data });
};
/** Schedulers视图---删除Schedulers视图 */
export const fetchDeleteSchedulers = (data: any) => {
return http.request<BaseResult<object>>('delete', 'schedulers/deleteSchedulers', { data });
};

View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 任务调度分组---获取任务调度分组列表 */
export const fetchGetSchedulersGroupList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `schedulersGroup/getSchedulersGroupList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 任务调度分组---获取所有任务调度分组 */
export const fetchGetAllSchedulersGroup = () => {
return http.request<BaseResult<ResultTable>>('get', 'schedulersGroup/getAllSchedulersGroup');
};
/** 任务调度分组---添加任务调度分组 */
export const fetchAddSchedulersGroup = (data: any) => {
return http.request<BaseResult<object>>('post', 'schedulersGroup/addSchedulersGroup', { data });
};
/** 任务调度分组---更新任务调度分组 */
export const fetchUpdateSchedulersGroup = (data: any) => {
return http.request<BaseResult<object>>('put', 'schedulersGroup/updateSchedulersGroup', { data });
};
/** 任务调度分组---删除任务调度分组 */
export const fetchDeleteSchedulersGroup = (data: any) => {
return http.request<BaseResult<object>>('delete', 'schedulersGroup/deleteSchedulersGroup', { data });
};

View File

@ -0,0 +1,125 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
export interface UserResult {
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
}
export interface RefreshTokenResult {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
}
/** 登录 */
export const fetchLogin = (data?: object) => {
return http.request<BaseResult<UserResult>>('post', '/login', { data });
};
/** 发送邮件 */
export const fetchPostEmailCode = (data: any) => {
return http.request<BaseResult<any>>('post', '/user/noAuth/sendLoginEmail', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<BaseResult<RefreshTokenResult>>('post', 'user/noAuth/refreshToken', { data });
};
/** 退出账户 */
export const fetchLogout = (data?: object) => {
return http.request<BaseResult<any>>('post', 'user/noManage/logout', { data });
};
/** 获取用户信息,根据当前token获取 */
export const fetchGetUserinfo = () => {
return http.request<BaseResult<any>>('get', 'user/noManage/getUserinfo');
};
/** 用户信息---获取用户信息列表 */
export const fetchGetAdminUserList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `user/getAdminUserList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 用户信息---查询用户 */
export const fetchQueryUser = (data: any) => {
return http.request<BaseResult<object>>('get', 'user/noManage/queryUser', { params: data });
};
/** 用户信息---更新用户信息 */
export const fetchUpdateAdminUser = (data: any) => {
return http.request<BaseResult<object>>('put', 'user/updateAdminUser', { data });
};
/** 用户信息---更新本地用户信息 */
export const fetchUpdateAdminUserByLocalUser = (data: any) => {
return http.request<BaseResult<object>>('put', 'user/noManage/updateAdminUserByLocalUser', { data });
};
/** 用户信息---更新本地用户密码 */
export const fetchUpdateUserPasswordByLocalUser = (data: any) => {
return http.request<BaseResult<object>>(
'put',
'user/noManage/updateUserPasswordByLocalUser',
{ data },
{ headers: { 'Content-Type': 'multipart/form-data' } },
);
};
/** 用户信息---添加用户信息 */
export const fetchAddAdminUser = (data: any) => {
return http.request<BaseResult<object>>('post', 'user/addAdminUser', { data });
};
/** 用户信息---删除用户信息 */
export const fetchDeleteAdminUser = (data: any) => {
return http.request<BaseResult<object>>('delete', 'user/deleteAdminUser', { data });
};
/** 用户管理---获取用户信息 */
export const fetchGetUserinfoById = (data?: object) => {
return http.request<BaseResult<UserResult>>('get', 'user/getUserinfoById', { params: data });
};
/** 用户管理---修改用户状态 */
export const fetchUpdateUserStatusByAdmin = (data?: object) => {
return http.request<BaseResult<UserResult>>('put', 'user/updateUserStatusByAdmin', { data });
};
/** 用户管理---管理员修改管理员用户密码 */
export const fetchUpdateUserPasswordByAdmin = (data: any) => {
return http.request<BaseResult<UserResult>>('put', 'user/updateUserPasswordByAdmin', { data });
};
/** 用户管理---管理员修改管理员用户头像 */
export const fetchUploadAvatarByAdmin = (data: any) => {
return http.request<BaseResult<UserResult>>('put', 'user/uploadAvatarByAdmin', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/** 用户管理---强制用户下线 */
export const fetchForcedOffline = (data: any) => {
return http.request<BaseResult<UserResult>>('put', 'user/forcedOffline', { data });
};
/** 用户管理---为用户分配角色 */
export const fetchAssignRolesToUsers = (data: object) => {
return http.request<BaseResult<any>>('post', 'userRole/assignRolesToUsers', { data });
};

27
src/api/v1/system/dept.ts Normal file
View File

@ -0,0 +1,27 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 部门管理---获取部门管理列表 */
export const fetchGetDeptList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `dept/getDeptList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 部门管理---获取所有部门管理列表 */
export const fetchGetAllDeptList = () => {
return http.request<BaseResult<object>>('get', 'dept/noManage/getAllDeptList');
};
/** 部门管理---添加部门管理 */
export const fetchAddDept = (data: any) => {
return http.request<BaseResult<object>>('post', 'dept/addDept', { data });
};
/** 部门管理---更新部门管理 */
export const fetchUpdateDept = (data: any) => {
return http.request<BaseResult<object>>('put', 'dept/updateDept', { data });
};
/** 部门管理---删除部门管理 */
export const fetchDeleteDept = (data: any) => {
return http.request<BaseResult<object>>('delete', 'dept/deleteDept', { data });
};

View File

@ -0,0 +1,37 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 权限---获取权限列表 */
export const fetchGetPowerList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `power/getPowerList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 权限---根据角色id获取权限内容 */
export const fetchGetPowerListByRoleId = (data: any) => {
return http.request<BaseResult<object>>('get', 'rolePower/noManage/getPowerListByRoleId', { params: data });
};
/** 权限---获取所有权限 */
export const fetchGetAllPowers = () => {
return http.request<BaseResult<any>>('get', `power/getAllPowers`);
};
/** 权限---添加权限 */
export const fetchAddPower = (data: any) => {
return http.request<BaseResult<object>>('post', 'power/addPower', { data });
};
/** 权限---更新权限 */
export const fetchUpdatePower = (data: any) => {
return http.request<BaseResult<object>>('put', 'power/updatePower', { data });
};
/** 权限---更新权限 */
export const fetchUpdateBatchByPowerWithParentId = (data: any) => {
return http.request<BaseResult<object>>('put', 'power/updateBatchByPowerWithParentId', { data });
};
/** 权限---删除权限 */
export const fetchDeletePower = (data: any) => {
return http.request<BaseResult<object>>('delete', 'power/deletePower', { data });
};

37
src/api/v1/system/role.ts Normal file
View File

@ -0,0 +1,37 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/** 角色---获取角色列表 */
export const fetchGetRoleList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `role/getRoleList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 角色---获取所有角色 */
export const fetchGetAllRoles = () => {
return http.request<BaseResult<any>>('get', `role/noManage/getAllRoles`);
};
/** 角色---根据用户id获取所有角色 */
export const fetchGetRoleListByUserId = data => {
return http.request<BaseResult<any>>('get', `userRole/getRoleListByUserId`, { params: data });
};
/** 角色---添加角色 */
export const fetchAddRole = (data: any) => {
return http.request<BaseResult<object>>('post', 'role/addRole', { data });
};
/** 角色---为角色分配权限 */
export const fetchAssignPowersToRole = (data: any) => {
return http.request<BaseResult<object>>('post', 'rolePower/assignPowersToRole', { data });
};
/** 角色---更新角色 */
export const fetchUpdateRole = (data: any) => {
return http.request<BaseResult<object>>('put', 'role/updateRole', { data });
};
/** 角色---删除角色 */
export const fetchDeleteRole = (data: any) => {
return http.request<BaseResult<object>>('delete', 'role/deleteRole', { data });
};

View File

@ -0,0 +1,12 @@
import { http } from '@/api/service/request';
import type { BaseResult } from '@/api/service/types';
/** 系统管理-用户路由获取 */
export const getRouterAsync = () => {
return http.request<BaseResult<any>>('get', 'router/noManage/getRouterAsync');
};
/** 上传文件 */
export const fetchUploadFile = (data: any) => {
return http.request<BaseResult<any>>('post', '/files/upload', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};

View File

@ -0,0 +1,27 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src:
url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.pure-iconfont-tabs:before {
content: "\e63e";
}
.pure-iconfont-logo:before {
content: "\e620";
}
.pure-iconfont-new:before {
content: "\e615";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
{
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795615",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

BIN
src/assets/login/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
width="1em" height="1em" viewBox="0 0 24 24" style="outline: none;">
<path fill="currentColor"
d="M14 8.947L22 14v2l-8-2.526v5.36l3 1.666V22l-4.5-1L8 22v-1.5l3-1.667v-5.36L3 16v-2l8-5.053V3.5a1.5 1.5 0 0 1 3 0v5.447Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 318 B

1
src/assets/svg/back.svg Normal file
View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 48 48"><path fill="#2F88FF" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="4" d="M44 40.836q-7.34-8.96-13.036-10.168t-10.846-.365V41L4 23.545 20.118 7v10.167q9.523.075 16.192 6.833 6.668 6.758 7.69 16.836Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-calendar" viewBox="0 0 16 16"><path fill="currentColor" d="M10 3H6V1.5H5V3H3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-2V1.5h-1zM5 5h1V4h4v1h1V4h2v2H3V4h2zM3 7h10v6H3z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

1
src/assets/svg/dark.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981"/></svg>

After

Width:  |  Height:  |  Size: 262 B

1
src/assets/svg/day.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="globalization" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/></svg>

After

Width:  |  Height:  |  Size: 826 B

1
src/assets/svg/hot.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 1024 1024"><path fill="#FF5D50" d="M428.698 107.315c-6.503 72.192-36.352 207.258-160.256 337.408 3.686-48.025-7.117-83.763-19.047-107.673-6.605-13.159-26.06-10.599-28.877 3.84-5.734 29.44-20.582 75.059-57.6 137.779-71.628 121.395-62.566 459.878 340.736 459.878S934.093 585.728 876.8 442.522c-37.376-93.44-93.952-152.525-128.82-182.324-11.417-9.779-29.132-1.945-29.593 13.056-.921 30.464-7.321 73.37-33.075 102.144-.666-52.787-38.144-208.384-202.445-296.857-23.296-12.544-51.763 2.457-54.17 28.774z"/><path fill="#FFDF99" d="M702.26 678.4c-4.2-45.056-60.673-166.554-212.634-246.426-10.599-5.58-23.092 3.124-21.504 15.002 6.246 46.848 12.953 140.493-24.064 184.73 4.044-40.397-18.125-73.83-36.66-94.31-8.396-9.217-23.552-4.66-25.497 7.68-3.533 22.322-12.851 56.268-36.557 97.945-42.086 74.035-86.989 188.672 124.57 294.656 10.956.563 22.17.87 33.74.87a618 618 0 0 0 32.717-.87C694.631 878.182 709.837 759.706 702.26 678.4"/></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-laptop" viewBox="0 0 16 16"><path fill="currentColor" d="M2.5 12a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1zm0-1h11V4h-11zM15 13H1v1h14z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,12 @@
<svg class="circular" viewBox="0 0 20 20">
<g
class="path2 loading-path"
stroke-width="0"
style="animation: none; stroke: none"
>
<circle r="3.375" class="dot1" rx="0" ry="0"/>
<circle r="3.375" class="dot2" rx="0" ry="0"/>
<circle r="3.375" class="dot4" rx="0" ry="0"/>
<circle r="3.375" class="dot3" rx="0" ry="0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-service" viewBox="0 0 16 16"><path fill="currentColor" d="M2.52 6.37a5.5 5.5 0 0 1 10.98.13v4c0 .05 0 .1-.02.15A4.5 4.5 0 0 1 9 14.7H8v-1h1a3.5 3.5 0 0 0 3.4-2.7h-1.9a.5.5 0 0 1-.5-.5v-4c0-.28.22-.5.5-.5h1.93a4.5 4.5 0 0 0-8.86 0H5.5c.28 0 .5.22.5.5v4a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5v-4c0-.04 0-.09.02-.13M12.5 7H11v3h1.5zm-9 0v3H5V7z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

1
src/assets/svg/shop.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-shop" viewBox="0 0 16 16"><path fill="currentColor" d="M8 1a2.5 2.5 0 0 0-2.5 2.5V5h-2a.5.5 0 0 0-.5.5v9c0 .28.22.5.5.5h9a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-2V3.5A2.5 2.5 0 0 0 8 1m1.5 5v2h1V6H12v8H4V6h1.5v2h1V6zm0-1h-3V3.5a1.5 1.5 0 1 1 3 0z"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" viewBox="0 0 1024 1024"><path d="M554 849.574c0 23.365-18.635 42.307-42 42.307s-42-18.941-42-42.307V662.719c0-23.365 18.635-42.307 42-42.307v-7.051c23.365 0 42 25.993 42 49.358z"/><path d="M893 888.5c0 17.397-14.103 31.5-31.5 31.5h-700c-17.397 0-31.5-14.103-31.5-31.5s14.103-31.5 31.5-31.5h700c17.397 0 31.5 14.103 31.5 31.5m33-714.074C926 135.484 894.686 105 855.744 105H168.256C129.314 105 98 135.484 98 174.426V533h828zM98 630.988C98 669.931 129.314 702 168.256 702h687.488C894.686 702 926 669.931 926 630.988V596H98z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 2H2v20h2v-9h14.17l-5.5 5.5 1.41 1.42L22 12l-7.92-7.92-1.41 1.42 5.5 5.5H4z"/></svg>

After

Width:  |  Height:  |  Size: 163 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-user-avatar" viewBox="0 0 16 16"><path fill="currentColor" d="M8 10.5c1.24 0 2.42.31 3.5.88v1.12h1v-1.14a.94.94 0 0 0-.49-.84 8.48 8.48 0 0 0-8.02 0 .94.94 0 0 0-.49.84v1.14h1v-1.12A7.5 7.5 0 0 1 8 10.5M10.5 6a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0m-1 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"/><path fill="currentColor" d="M2.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-11a1 1 0 0 0-1-1zm11 1v11h-11v-11z"/></svg>

After

Width:  |  Height:  |  Size: 482 B

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