Compare commits

...

123 Commits

Author SHA1 Message Date
ktianc
dd2e3a65af 更新二维码 2025-03-25 09:32:44 +08:00
ktianc
7adc37990a 版本信息说明更新 2024-12-28 13:29:01 +08:00
ktianc
8a75c11182 不影响的微调 2024-09-06 15:32:26 +08:00
ktianc
7fd7366e32 新增接口部分初始化失败的文档 2024-08-27 10:54:10 +08:00
ktianc
1354ea21e4 merge pr !20 添加了kinit-api 的 readme:数据迁移 、CRUD 2024-07-05 10:21:17 +08:00
ktianc
2d6108be44 Merge branch 'zsr_20240620' of https://gitee.com/westernCloud/kinit 2024-07-05 10:16:43 +08:00
zhusr39924
ef5c73b468 更新readme:数据迁移和生成CRUD,路由配置 2024-07-03 11:08:48 +08:00
zhusr39924
afe3b042d9 更新readme:数据迁移和生成CRUD,路由配置 2024-07-03 10:41:54 +08:00
zhusr39924
5af56e956c 更新readme:数据迁移和生成CRUD,路由配置 2024-07-03 10:34:54 +08:00
ktianc
1cc3fb0c3b update README.md 2024-07-03 09:25:24 +08:00
ktianc
39194d51c0 修复版本 2024-04-25 10:26:48 +08:00
ktianc
8d582b482c 前端路由 alwaysShow 不起作用问题修复 2024-04-25 10:23:27 +08:00
ktianc
13e124c02a 异常修复 2024-04-25 09:02:23 +08:00
ktianc
10ea077735 更新群二维码 2024-04-25 08:59:15 +08:00
ktianc
e191f76809 更新群链接 2024-04-21 22:33:35 +08:00
ktianc
e91dca078b 更新群链接 2024-04-14 22:13:22 +08:00
ktianc
e1d388dbfc 更新群链接 2024-04-06 16:35:49 +08:00
ktianc
39cd05fb44 更新群二维码 2024-03-30 16:25:59 +08:00
ktianc
8364ef731e
!18 3.10.0
Merge pull request !18 from ktianc/master
2024-03-23 10:05:56 +00:00
ktianc
cff85f09d5 更新群二维码 2024-03-23 18:02:29 +08:00
ktianc
eff099877e 优化忽略文件 2024-03-23 17:38:05 +08:00
ktianc
303777910c 清理无用文件 2024-03-23 16:57:58 +08:00
ktianc
76bd8ed3fe 微信小程序优化 2024-03-23 16:52:47 +08:00
ktianc
53c12a787e 优化忽略文件 2024-03-23 16:48:23 +08:00
ktianc
2970fe16da 修复声明返回类型错误 2024-03-21 21:42:56 +08:00
ktianc
4229620d8d
Merge pull request #25 from yaqiangsun/master
Fix alembic.ini config in README.md
2024-03-18 17:35:19 +08:00
yaqiang.sun
9c148dbb6b
Fix alembic.ini config in README.md
Remove extra 'sqlalchemy.url =' in alembic.ini config in README.md
2024-03-18 16:10:24 +08:00
ktianc
74c0b0a484 Merge branch 'master' of https://gitee.com/ktianc/kinit_dev 2024-03-17 12:17:46 +08:00
ktianc
974322a2b8
!17 更新群二维码
Merge pull request !17 from ktianc/master
2024-03-17 04:16:44 +00:00
ktianc
7ca8bd3244
Revert "add chat 1"
This reverts commit 9f635dc5f4b137fda6b6d73af0fdcb40562ac97c.
2024-03-17 04:14:51 +00:00
ktianc
332e728ca3
Revert "更新1"
This reverts commit 53a694be367e0b30a1070e9a72476cf01be8db90.
2024-03-17 04:14:35 +00:00
ktianc
bc5fa85ba6
Revert "Revert "更新至 3.9.0""
This reverts commit 03d8bd2eb4ae77ed2e57aa55635972b43515b10c.
2024-03-17 04:13:30 +00:00
ktianc
aafcb6527f Merge branch 'master' of https://gitee.com/ktianc/kinit_dev 2024-03-17 12:11:22 +08:00
ktianc
03d8bd2eb4
Revert "更新至 3.9.0"
This reverts commit adc7b21fc287c8d74ddd804c9be0d6c177793b9d.
2024-03-17 04:10:52 +00:00
ktianc
7a33c4d4f7 更新群二维码 2024-03-17 11:55:06 +08:00
ktianc
bc5f239cb7 更新群链接 2024-03-17 11:21:14 +08:00
ktianc
53a694be36 更新1 2024-03-15 17:39:17 +08:00
ktianc
9f635dc5f4 add chat 1 2024-03-13 18:57:53 +08:00
ktianc
b3b427edef
!16 3.9.0
Merge pull request !16 from ktianc/master
2024-03-12 10:26:14 +00:00
ktianc
adc7b21fc2 更新至 3.9.0 2024-03-12 18:24:55 +08:00
ktianc
61f39a7c64 vue-element-plus-admin 框架更新至最新版本 3.7.0(3.7.0中新增功能未更新只修复了问题) 2024-03-12 18:23:03 +08:00
ktianc
13bfb7d7b8
!15 3.8.1
Merge pull request !15 from ktianc/master
2024-03-09 08:59:47 +00:00
ktianc
8265cbc6d0 修复使用自定义数据权限报错问题 2024-03-09 12:30:18 +08:00
ktianc
4d240c24d2
!14 v3.8.1
Merge pull request !14 from ktianc/master
2024-03-08 08:35:32 +00:00
ktianc
2c39c91108 更新版本 2024-03-08 16:33:40 +08:00
ktianc
df611901c6 更新群相册 2024-03-08 16:32:19 +08:00
ktianc
ae44370f78 增加快捷操作 2024-03-08 16:32:03 +08:00
ktianc
149812914e 修复 oss 文件上传路径错误问题 2024-03-08 16:31:14 +08:00
ktianc
b08cd1ca42
!13 更新至 v3.8.0
Merge pull request !13 from ktianc/master
2024-03-07 09:59:17 +00:00
ktianc
1e7dbec10b 更新版本 2024-03-07 09:01:23 +08:00
ktianc
9bb0d17fb8 部分代码优化,接口依赖包升级 2024-03-06 19:05:49 +08:00
ktianc
953cdda006 新增用户密码加入请求校验,如果密码发生改变需要重新登陆,便于使用密码进行强制退出 2024-03-06 19:04:20 +08:00
ktianc
3ccbc2c4b2 新增演示接口黑名单功能 2024-03-06 18:59:29 +08:00
ktianc
057915375a 登录日志列表获取倒叙 2024-03-06 16:47:13 +08:00
ktianc
64f221fb3f
!12 3.7.1
Merge pull request !12 from ktianc/master
2024-03-02 12:02:24 +00:00
ktianc
0189fa867a 更新版本号,默认配置为使用演示环境 2024-03-02 19:56:40 +08:00
ktianc
c5cfe3ffcb 更新群二维码 2024-03-02 19:56:01 +08:00
ktianc
5053d59f62 前端优化 2024-03-02 19:55:23 +08:00
ktianc
f8c748a15a 修复系统基本信息修改报错问题 2024-03-02 19:54:31 +08:00
ktianc
7036c1fc02 异步上传保存文件到本地问题修复 2024-03-02 19:53:24 +08:00
ktianc
20a425ed8c 更新群二维码 2024-02-25 16:53:15 +08:00
ktianc
3054471123
!11 3.7.0
Merge pull request !11 from ktianc/master
2024-02-25 08:42:21 +00:00
ktianc
cab33828dd 1. 取消TEMP静态目录挂载
2. 修复用户数据导出功能
3. 生成 static 静态目录将时间戳改为日期
2024-02-25 16:40:19 +08:00
ktianc
3adfa91560 新增单元格填充颜色默认为白色 2024-02-25 16:34:24 +08:00
ktianc
65c204287f 更新 2024-02-19 23:08:55 +08:00
ktianc
aca6c3d4f9
!10 FileManage 保存文件到本地后,输出文件路径错误问题解决
Merge pull request !10 from ktianc/master
2024-02-19 14:58:18 +00:00
ktianc
3f8b0efccc FileManage 保存文件到本地后,输出文件路径错误问题解决 2024-02-19 22:56:52 +08:00
ktianc
364a5b3491 更新群二维码 2024-02-19 22:33:23 +08:00
lizhikang
a632d7cfd7 更新版本 2024-02-09 20:31:10 +08:00
ktianc
09abd68f8e
!9 修复 WriteXlsx 使用 static 创建默认 excel 问题
Merge pull request !9 from ktianc/master
2024-02-07 13:50:49 +00:00
lizhikang
56757b8101 修复 WriteXlsx 使用 static 创建默认 excel 问题 2024-02-07 21:48:35 +08:00
ktianc
19a19b4533
!8 更新群二维码
Merge pull request !8 from ktianc/master
2024-02-07 09:16:56 +00:00
lizhikang
b4d0ed3a3e 更新群二维码 2024-02-07 17:16:11 +08:00
ktianc
cab330dd77
!7 完善 docker-compose 部署描述
Merge pull request !7 from ktianc/master
2024-02-07 09:15:28 +00:00
lizhikang
8bcfbe7512 完善 docker-compose 部署描述 2024-02-07 17:08:24 +08:00
ktianc
675592a5a8 新增微信技术讨论群 2024-01-30 19:18:59 +08:00
ktianc
9ca8e6911b 小版本更新 2024-01-26 20:46:11 +08:00
ktianc
b013ac4a87 修复异常输出错误 2024-01-26 20:28:30 +08:00
ktianc
851d66d594 更新注释内容 2024-01-26 20:21:03 +08:00
ktianc
cb33aa470e 更新 README 描述 2024-01-25 22:10:57 +08:00
ktianc
28487cd02c 清理无用文件 2024-01-25 22:10:27 +08:00
ktianc
edab311363 页面优化 2024-01-25 22:10:10 +08:00
ktianc
ece609662a 修复个人主页404问题 2024-01-25 22:04:46 +08:00
ktianc
35fff39af1 调度日志任务返回值反序列化展示,以及修复详情窗口报错问题 2024-01-22 22:33:17 +08:00
ktianc
e08f0e153c 修复关闭页面重新打开,无法进入动态路由问题 2024-01-22 22:32:14 +08:00
ktianc
01f1a9e88e 更新readme:加入视频演示地址 2024-01-21 16:34:28 +08:00
ktianc
254d0e5958 补充更新 2024-01-21 12:20:59 +08:00
ktianc
7329231c61 优化 2024-01-21 10:56:01 +08:00
ktianc
76a5b9b467 更新版本 2024-01-21 10:55:47 +08:00
ktianc
cde6b1b497 readme 更新,新增接口 crud 代码自动生成用例 2024-01-21 10:54:45 +08:00
ktianc
5d9801dbd7 更新依赖库 2024-01-21 10:54:06 +08:00
ktianc
a5476a92d3 新增多种数据操作测试用例,可用来作为参考 2024-01-21 10:53:37 +08:00
ktianc
7eb590a697 新增 crud 代码自动生成 2024-01-21 10:52:47 +08:00
ktianc
65f92947f5 初始化数据更新 2024-01-21 10:52:25 +08:00
ktianc
244da5fdd5 语法优化,并新增支持会话过期 2024-01-21 10:51:55 +08:00
ktianc
ebc0095ca6 新增 mongo 与 redis 数据库连接检查 2024-01-21 10:50:27 +08:00
ktianc
9ceeacb97b 更新 vue-element-plus-admin 到最新版本 2.5.6 2024-01-21 10:16:32 +08:00
ktianc
bade36dd1b 新增部门管理功能,角色权限支持部门数据权限划分,接口权限认证加入部门数据权限字段 2024-01-05 11:36:04 +08:00
ktianc
d16382c90c ORM 模型基类新增获取字段列表方法 2024-01-05 11:35:09 +08:00
ktianc
e026182838 ppt 转 pdf 功能分离,不默认导入 2024-01-05 11:34:21 +08:00
ktianc
905dccc243 socket client 添加 tcp 通信协议 2024-01-05 11:33:35 +08:00
ktianc
a90ed5c4ea 取消图片压缩功能 2024-01-05 11:32:58 +08:00
ktianc
a44c7c2bae 修复点击调度任务失败问题 2023-12-21 16:27:40 +08:00
ktianc
1396520ea3 优化后的遗留问题修复 2023-12-19 00:25:01 +08:00
ktianc
0ec1876584 更新依赖库 2023-12-16 22:06:44 +08:00
ktianc
72f614ed12 更新版本 2023-12-16 21:17:55 +08:00
ktianc
29037d7184 优化crud 增加类型提示 2023-12-16 21:17:44 +08:00
ktianc
518f9d4a47 优化核心类 2023-12-16 21:16:49 +08:00
ktianc
26ffb4c167 新增 socker 客户端工具类 2023-12-16 21:16:27 +08:00
ktianc
d9ffa98d13 优化工具类 2023-12-16 21:16:01 +08:00
ktianc
713f4bdcda 前端依赖库改为固定版本号 2023-12-05 23:17:35 +08:00
ktianc
21980a6e34 优化:异常处理 2023-11-16 13:47:07 +08:00
ktianc
4a9bf1fdc3 优化:文件操作模块 2023-11-16 13:46:36 +08:00
ktianc
0a1b4f1881 更新:接口依赖库 2023-11-16 13:45:35 +08:00
ktianc
7a8cca62e0 新增:自定义接口文档静态文件(本地存储) 2023-11-16 13:45:11 +08:00
ktianc
3bf7a5a3a3 更新菜单前端组件字段长度,以及用户模型类方法优化 2023-11-05 14:50:33 +08:00
ktianc
08c681e608 alembic 迁移数据库加入支持检查字段类型,字段长度,数据库字段默认值 2023-11-05 14:49:40 +08:00
ktianc
66806dac91 update version 2023-10-28 21:52:35 +08:00
ktianc
dc34f018b3
!6 修改捕获异常后日志记录信息
Merge pull request !6 from 刘建/N/A
2023-10-28 13:41:42 +00:00
刘建
43ee394c21
修改捕获异常后日志记录信息
Signed-off-by: 刘建 <liujian_8670@qq.com>
2023-10-27 07:43:25 +00:00
ktianc
572f494a88 fix: 系统配置中新增短信配置窗口 2023-10-18 19:02:50 +08:00
ktianc
54f8671899 同步升级至 vue-element-plus-admin 2.3.0 版本 2023-10-17 21:56:34 +08:00
ktianc
61e9a336d3 FastAPI 依赖库同步升级,官方升级版本文档:https://fastapi.tiangolo.com/release-notes/,本次主要升级内容:https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/ 2023-10-17 14:55:47 +08:00
531 changed files with 11990 additions and 25324 deletions

21
.gitignore vendored
View File

@ -1,22 +1 @@
# Build and Release Folders
bin-debug/
bin-release/
[Oo]bj/
[Bb]in/
# Other files and folders
.settings/
# Executables
*.swf
*.air
*.ipa
*.apk
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
# should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder.
docker_env/mysql/data/
docker_env/redis/data/
*/.idea
dvadmin-doc/docs/.vuepress/dist

116
README.md
View File

@ -21,10 +21,11 @@
Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
- 后端采用现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + 自动生成交互式API文档 + (强制类型约束)[Pydantic](https://docs.pydantic.dev/1.10/) + (高效率)[SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/index.html)
- PC端采用 [vue-element-plus-admin 2.2.0](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/)等主流技术开发;
- PC端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/)等主流技术开发;
- 移动端采用 [uni-app](https://uniapp.dcloud.net.cn/component/)[Vue2](https://v2.cn.vuejs.org/v2/guide/)[uView 2](https://www.uviewui.com/components/intro.html)为主要技术开发;
- 后端加入 [Typer](https://typer.tiangolo.com/) 命令行应用,简单化数据初始化,数据表模型迁移等操作;
- 已加入定时任务功能,采用 [APScheduler](https://github.com/agronholm/apscheduler) 定时任务框架 + [Redis](https://redis.io/) 消息队列 + [MongoDB](https://www.mongodb.com/) 持久存储;
- 后端新加入根据配置的 ORM 模型,自动生成 CRUD 代码;
- 定时任务功能,采用 [APScheduler](https://github.com/agronholm/apscheduler) 定时任务框架 + [Redis](https://redis.io/) 消息队列 + [MongoDB](https://www.mongodb.com/) 持久存储;
- 权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。
- 支持加载动态权限菜单,多方式轻松权限控制,按钮级别权限控制。
- 已加入常见的 [MySQL](https://www.mysql.com/) + [MongoDB](https://www.mongodb.com/) + [Redis](https://redis.io/) 数据库异步操作。
@ -42,6 +43,27 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
[小诺开源技术 (xiaonuo.vip)](https://www.xiaonuo.vip/):国内首个国密前后端分离快速开发平台
## 微信群
提供一个技术交流群,现在还没什么人哈哈哈哈哈,真心希望大家能够加入,积极讨论,因为本项目中还没有详细使用文档(一直在欠着,我也挺不好意思的),所以大家加入后,也可以很方便的一起讨论在使用中遇到各种问题,也可以提一些你想加入的功能,让我们更近一点,欢迎大家的加入。
2024-4-25 目前群聊已经达到 200 人,只能通过邀请进群,不能再直接扫描群二维码进群,需要进群的可以先加我,备注进群,我就拉你进群。
<div align="center">
<p align="center">
<img src="https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/public/images/WechatIMG285.jpg" height="500" alt="logo"/>
</p>
</div>
## 在线体验
PC端演示地址https://kinit.ktianc.top
@ -58,9 +80,71 @@ PC端演示地址https://kinit.ktianc.top
</div>
管理员账户:
- 账号15020221010
- 密码kinit2022
测试账户:
- 账号15020240125
- 密码test
## 接口 CURD 代码自动生成
1. 目前只支持生成接口代码
2. 目前只支持使用脚本方式运行,后续会更新到页面操作
3. 代码是根据手动配置的 ORM 模型来生成的,支持参数同步,比如默认值,是否为空...
脚本文件地址:`scripts/crud_generate/main.py`
该功能首先需要手动创建出 ORM 模型,然后会根据 ORM 模型依次创建代码,包括如下代码:
1. schema 序列化代码
schema 文件名称会使用设置的 en_name 名称,如果文件已经存在会先执行删除,再创建。
schema 代码内容生成完成后,同时会将新创建的 class 在 `__init__.py` 文件中导入。
2. dal 数据操作代码
dal 文件名称会使用默认的 `crud.py` 文件名称,目前不支持自定义。
如果 dal 文件已经存在,并且已经有代码内容,那么会将新的模型 dal class 追加到文件最后,并会合并文件内导入的 module。
3. param 请求参数代码
param 文件名取名方式与 schema 一致。
会创建出默认最简的 param class。
4. view 视图代码
view 文件名称同样会使用默认的 `view.py` 文件名称,目前不支持自定义。
如果 view 文件已经存在,与 dal 执行操作一致。
脚本中目前有两个方法:
```python
if __name__ == '__main__':
from apps.vadmin.auth.models import VadminUser
crud = CrudGenerate(VadminUser, "用户", "user")
# 只打印代码,不执行创建写入
crud.generate_codes()
# 创建并写入代码
crud.main()
```
目前不会去检测已有的代码,比如 `UserDal` 已经存在,还是会继续添加的。
B站 视频演示https://www.bilibili.com/video/BV19e411a7zP/
## 源码地址
gitee地址(主推)https://gitee.com/ktianc/kinit
@ -71,7 +155,9 @@ github地址https://github.com/vvandk/kinit
- [x] 菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
- [x] 角色管理:角色菜单权限分配。
- [x] 部门管理:支持无限层级部门配置。
- [x] 角色管理:角色菜单权限,角色部门权限分配。
- [x] 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
@ -171,10 +257,14 @@ git clone https://gitee.com/ktianc/kinit.git
### 准备工作
```
Python == 3.10 (其他版本均未测试)
nodejs >= 14.0 (推荐使用最新稳定版)
Mysql >= 8.0
MongoDB (推荐使用最新稳定版)
后端依赖版本:
Python == 3.10.x (其他版本均未测试)
前端依赖版本:
nodejs >= 18.0 < 19
pnpm >= 8.1.0 < 9
数据库版本:
Mysql >= 8.0 (8 以上未测试以下版本未测试postgresql 未测试,更换可能会涉及调整)
MongoDB >= 7.0.12 < 8 (7 以上或以下版本均未测试)
Redis (推荐使用最新稳定版)
```
@ -195,6 +285,7 @@ Redis (推荐使用最新稳定版)
```python
# 安全警告: 不要在生产中打开调试运行!
DEBUG = True # 如果当前为开发环境则改为 True如果为生产环境则改为 False
```
3. 修改项目数据库配置信息
@ -242,6 +333,7 @@ Redis (推荐使用最新稳定版)
# 文档https://user.ip138.com/ip/doc
IP_PARSE_ENABLE = True
IP_PARSE_TOKEN = "IP_PARSE_TOKEN"
```
4. 并在`alembic.ini`文件中配置数据库信息,用于数据库映射
@ -251,13 +343,13 @@ Redis (推荐使用最新稳定版)
[dev]
# 开发环境
version_locations = %(here)s/alembic/versions_dev
sqlalchemy.url = sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
[pro]
# 生产环境
version_locations = %(here)s/alembic/versions_pro
sqlalchemy.url = sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
```
5. 创建数据库
@ -413,7 +505,9 @@ pnpm run build:pro
DEBUG = False # 生产环境应该改为 False
```
3. 如果已有 Mysql 或者 Redis 或者 MongoDB 数据库,请修改如下内容,如果没有则不需要修改:
3. **如果没有安装数据库则不需要这一操作**)如果已有 Mysql 或者 Redis 或者 MongoDB 数据库,请执行以下操作:
请先在对应数据库中创建用户名以及数据库,并修改以下数据库连接改为已有的数据库连接
1. 修改 API 端配置文件:
@ -422,7 +516,7 @@ pnpm run build:pro
```python
# Mysql 数据库配置项
# 连接引擎官方文档https://www.osgeo.cn/sqlalchemy/core/engines.html
# 数据库接配置说明mysql+asyncmy://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称
# 数据库接配置说明mysql+asyncmy://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称
SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@177.8.0.7:3306/kinit"
# Redis 数据库配置

View File

@ -1,8 +1,8 @@
# 环境
NODE_ENV=development
VITE_NODE_ENV=development
# 接口前缀
VITE_API_BASE_PATH=dev
# 接口前缀,没用到
# VITE_API_BASE_PATH=/api
# 打包路径
VITE_BASE_PATH=/
@ -21,3 +21,15 @@ VITE_OUT_DIR=dist-dev
# 标题
VITE_APP_TITLE=后台系统-开发
# 是否切割css
VITE_USE_CSS_SPLIT=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=true
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=true

View File

@ -1,8 +1,8 @@
# 环境
NODE_ENV=production
VITE_NODE_ENV=production
# 接口前缀
VITE_API_BASE_PATH=pro
# 接口前缀,没用到
# VITE_API_BASE_PATH=/api
# 打包路径
VITE_BASE_PATH=/
@ -21,3 +21,15 @@ VITE_OUT_DIR=dist-pro
# 标题
VITE_APP_TITLE=后台系统
# 是否切割css
VITE_USE_CSS_SPLIT=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=true
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false

View File

@ -65,6 +65,7 @@ module.exports = defineConfig({
}
],
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off'
'vue/no-v-html': 'off',
'vue/require-toggle-inside-transition': 'off'
}
})

View File

@ -1,7 +1,9 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
*-lock.*
pnpm-debug
stats.html
dist-pro
.vscode

View File

@ -3,7 +3,6 @@
/dist*
/public/*
/docs/*
/vite.config.ts
/src/types/env.d.ts
/docs/**/*
/plop/**/*

View File

@ -1,3 +0,0 @@
{
"recommendations": ["vue.volar", "lokalise.i18n-ally"]
}

View File

@ -1,19 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.enable": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[vue]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": false,
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"god.tsconfig": "./tsconfig.json"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.glob('./**/*.ts', {
import: 'default',
eager: true
})
const mockModules: any[] = []
Object.keys(modules).forEach(async (key) => {
if (key.includes('_')) {
return
}
mockModules.push(...(modules[key] as any))
})
export function setupProdMockServer() {
createProdMockServer(mockModules)
}

View File

@ -1,115 +1,127 @@
{
"name": "vue-element-plus-admin",
"version": "2.2.0",
"version": "2.7.0",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>",
"private": false,
"scripts": {
"i": "pnpm install",
"dev": "vite --mode dev",
"ts:check": "vue-tsc --noEmit --skipLibCheck",
"build:pro": "vite build --mode pro",
"build:dev": "vite build --mode dev",
"serve:pro": "vite preview --mode pro",
"serve:dev": "vite preview --mode dev",
"npm:check": "npx npm-check-updates",
"clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache",
"dev": "pnpm vite --mode dev",
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
"build:pro": "pnpm vite build --mode pro",
"build:dev": "pnpm vite build --mode dev",
"serve:pro": "pnpm vite preview --mode pro",
"serve:dev": "pnpm vite preview --mode dev",
"npm:check": "pnpx npm-check-updates -u",
"clean": "pnpx rimraf node_modules",
"clean:cache": "pnpx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"",
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"p": "plop"
"p": "plop",
"icon": "esno ./scripts/icon.ts"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.3.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"driver.js": "^1.2.1",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.9",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.3",
"qs": "^6.11.2",
"sortablejs": "^1.15.0",
"url": "^0.11.1",
"vue": "3.3.4",
"vue-i18n": "9.2.2",
"vue-json-pretty": "^2.2.4",
"vue-router": "^4.2.4",
"vue-types": "^5.1.1"
"@iconify/iconify": "3.1.1",
"@iconify/vue": "4.1.1",
"@vueuse/core": "10.9.0",
"@wangeditor/editor": "5.1.23",
"@wangeditor/editor-for-vue": "5.1.10",
"@zxcvbn-ts/core": "3.0.4",
"animate.css": "4.1.1",
"axios": "1.6.7",
"cropperjs": "1.6.1",
"dayjs": "1.11.10",
"driver.js": "1.3.1",
"echarts": "5.5.0",
"echarts-wordcloud": "2.1.0",
"element-plus": "2.5.6",
"lodash-es": "4.17.21",
"mitt": "3.0.1",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "3.2.1",
"qrcode": "1.5.3",
"qs": "6.11.2",
"url": "0.11.3",
"vue": "3.4.20",
"vue-draggable-plus": "0.3.5",
"vue-i18n": "9.9.1",
"vue-json-pretty": "2.3.0",
"vue-router": "4.3.0",
"vue-types": "5.1.1",
"xgplayer": "3.0.13"
},
"devDependencies": {
"@iconify/json": "^2.2.101",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@kjgl77/datav-vue3": "^1.6.1",
"@purge-icons/generated": "^0.9.0",
"@types/lodash-es": "^4.17.8",
"@types/node": "^20.4.10",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.1",
"@types/qs": "^6.9.7",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"@unocss/transformer-variant-group": "^0.55.0",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"autoprefixer": "^10.4.14",
"consola": "^3.2.3",
"cron-validate": "^1.4.5",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-define-config": "^1.23.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"intro.js": "^7.2.0",
"less": "^4.2.0",
"lint-staged": "^13.2.3",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"plop": "^3.1.2",
"postcss": "^8.4.27",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^3.0.1",
"rimraf": "^5.0.1",
"rollup": "^3.28.0",
"stylelint": "^15.10.2",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.19.2",
"typescript": "5.1.6",
"unocss": "^0.55.0",
"vite": "4.4.9",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "2.9.6",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2",
"@amap/amap-jsapi-loader": "1.0.1",
"@commitlint/cli": "19.0.1",
"@commitlint/config-conventional": "19.0.0",
"@iconify/json": "2.2.187",
"@intlify/unplugin-vue-i18n": "2.0.0",
"@kjgl77/datav-vue3": "1.6.1",
"@types/fs-extra": "11.0.4",
"@types/inquirer": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/node": "20.11.21",
"@types/nprogress": "0.2.3",
"@types/qrcode": "1.5.5",
"@types/qs": "6.9.12",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0",
"@unocss/transformer-variant-group": "0.58.5",
"@vitejs/plugin-legacy": "5.3.1",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0",
"autoprefixer": "10.4.17",
"chalk": "5.3.0",
"consola": "3.2.3",
"cron-validate": "1.4.5",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-define-config": "2.1.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-vue": "9.22.0",
"esno": "4.0.0",
"fs-extra": "11.2.0",
"inquirer": "9.2.15",
"less": "4.2.0",
"lint-staged": "15.2.2",
"lodash": "4.17.21",
"moment": "2.30.1",
"plop": "4.0.1",
"postcss": "8.4.35",
"postcss-html": "1.6.0",
"postcss-less": "6.0.0",
"prettier": "3.2.5",
"rimraf": "5.0.5",
"rollup": "4.12.0",
"rollup-plugin-visualizer": "5.12.0",
"stylelint": "16.2.1",
"stylelint-config-html": "1.1.0",
"stylelint-config-recommended": "14.0.0",
"stylelint-config-standard": "36.0.0",
"stylelint-order": "6.0.4",
"terser": "5.28.1",
"typescript": "5.3.3",
"unocss": "0.58.5",
"vite": "5.1.4",
"vite-plugin-ejs": "1.7.0",
"vite-plugin-eslint": "1.8.1",
"vite-plugin-progress": "0.0.7",
"vite-plugin-purge-icons": "0.10.0",
"vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue-draggable-plus": "^0.2.6",
"vue-tsc": "^1.8.8",
"vue3-json-viewer": "^2.2.2"
"vite-plugin-svg-icons": "2.0.1",
"vue-draggable-plus": "0.2.6",
"vite-plugin-url-copy": "1.1.3",
"vue-tsc": "1.8.27",
"vue3-json-viewer": "2.2.2",
"zipson": "0.2.12"
},
"packageManager": "pnpm@8.1.0",
"engines": {
"node": ">= 14.18.0"
"node": ">=18 <19",
"pnpm": ">=8.1.0 <10.0.0"
},
"license": "MIT",
"repository": {

View File

@ -0,0 +1,71 @@
import path from 'path'
import fs from 'fs-extra'
import inquirer from 'inquirer'
import chalk from 'chalk'
import pkg from '../package.json'
interface Icon {
name: string
prefix: string
icons: string[]
}
async function generateIcon() {
const dir = path.resolve(process.cwd(), 'node_modules/@iconify/json')
const raw = await fs.readJSON(path.join(dir, 'collections.json'))
const collections = Object.entries(raw).map(([id, v]) => ({
...(v as any),
id
}))
const choices = collections.map((item) => ({ key: item.id, value: item.id, name: item.name }))
inquirer
.prompt([
// {
// type: 'list',
// name: 'useType',
// choices: [
// { key: 'local', value: 'local', name: 'Local' },
// { key: 'onLine', value: 'onLine', name: 'OnLine' }
// ],
// message: 'How to use icons?'
// },
{
type: 'list',
name: 'iconSet',
choices: choices,
message: 'Select the icon set that needs to be generated?'
}
])
// ↓命令行问答的答案
.then(async (answers) => {
const { iconSet } = answers
// const isOnLine = useType === 'onLine'
const outputDir = path.resolve(process.cwd(), 'src/components/IconPicker/src/data')
fs.ensureDir(outputDir)
const genCollections = collections.filter((item) => [iconSet].includes(item.id))
const prefixSet: string[] = []
for (const info of genCollections) {
const data = await fs.readJSON(path.join(dir, 'json', `${info.id}.json`))
if (data) {
const { prefix } = data
const icons = Object.keys(data.icons).map((item) => `${prefix}:${item}`)
await fs.writeFileSync(
path.join('src/components/IconPicker/src/data', `icons.${prefix}.ts`),
`export default ${JSON.stringify({ name: info.name, prefix, icons })}`
)
// ↓分类处理完成push类型名称
prefixSet.push(prefix)
}
}
console.log(
`${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`
)
})
}
generateIcon()

View File

@ -2,9 +2,7 @@
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { ConfigGlobal } from '@/components/ConfigGlobal'
import { isDark } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { useStorage } from '@/hooks/web/useStorage'
import { getSystemBaseConfigApi } from '@/api/vadmin/system/settings'
const { getPrefixCls } = useDesign()
@ -17,18 +15,6 @@ const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
const { getStorage } = useStorage()
//
const setDefaultTheme = () => {
if (getStorage('isDark') !== null) {
appStore.setIsDark(getStorage('isDark'))
return
}
const isDarkTheme = isDark()
appStore.setIsDark(isDarkTheme)
}
// mate
const addMeta = (name: string, content: string) => {
const meta = document.createElement('meta')
@ -39,6 +25,9 @@ const addMeta = (name: string, content: string) => {
//
const setSystemConfig = async () => {
if (appStore.getLogoImage) {
return
}
const res = await getSystemBaseConfigApi()
if (res) {
appStore.setTitle(res.data.web_title || import.meta.env.VITE_APP_TITLE)
@ -52,7 +41,7 @@ const setSystemConfig = async () => {
}
}
setDefaultTheme()
appStore.initTheme()
setSystemConfig()
</script>

View File

@ -0,0 +1,25 @@
import request from '@/config/axios'
export const getDeptListApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/auth/depts', params })
}
export const delDeptListApi = (data: any): Promise<IResponse> => {
return request.delete({ url: '/vadmin/auth/depts', data })
}
export const addDeptListApi = (data: any): Promise<IResponse> => {
return request.post({ url: '/vadmin/auth/depts', data })
}
export const putDeptListApi = (data: any): Promise<IResponse> => {
return request.put({ url: `/vadmin/auth/depts/${data.id}`, data })
}
export const getDeptTreeOptionsApi = (): Promise<IResponse> => {
return request.get({ url: '/vadmin/auth/dept/tree/options' })
}
export const getDeptUserTreeOptionsApi = (): Promise<IResponse> => {
return request.get({ url: '/vadmin/auth/dept/user/tree/options' })
}

View File

@ -1,7 +1,7 @@
import request from '@/config/axios'
export const getSystemSettingsTabsApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/tabs', params })
export const getSystemSettingsTabsApi = (data: any): Promise<IResponse> => {
return request.post({ url: '/vadmin/system/settings/tabs', data })
}
export const getSystemSettingsApi = (params: any): Promise<IResponse> => {

View File

@ -0,0 +1,3 @@
import BaseButton from './src/Button.vue'
export { BaseButton }

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import { useDesign } from '@/hooks/web/useDesign'
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
import { PropType, Component, computed, unref } from 'vue'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const getTheme = computed(() => appStore.getTheme)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('button')
const props = defineProps({
size: {
type: String as PropType<ComponentSize>,
default: undefined
},
type: {
type: String as PropType<ButtonType>,
default: 'default'
},
disabled: {
type: Boolean,
default: false
},
plain: {
type: Boolean,
default: false
},
text: {
type: Boolean,
default: false
},
bg: {
type: Boolean,
default: false
},
link: {
type: Boolean,
default: false
},
round: {
type: Boolean,
default: false
},
circle: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: [String, Object] as PropType<String | Component>,
default: undefined
},
icon: {
type: [String, Object] as PropType<String | Component>,
default: undefined
},
autofocus: {
type: Boolean,
default: false
},
nativeType: {
type: String as PropType<'button' | 'submit' | 'reset'>,
default: 'button'
},
autoInsertSpace: {
type: Boolean,
default: false
},
color: {
type: String,
default: ''
},
darker: {
type: Boolean,
default: false
},
tag: {
type: [String, Object] as PropType<String | Component>,
default: 'button'
}
})
const emits = defineEmits(['click'])
const color = computed(() => {
const { type, link } = props
if (type === 'primary' && !link) {
return unref(getTheme).elColorPrimary
}
return ''
})
const style = computed(() => {
const { type, link } = props
if (type === 'primary' && !link) {
return '--el-button-text-color: #fff; --el-button-hover-text-color: #fff'
}
return ''
})
</script>
<template>
<ElButton
:class="`${prefixCls} color-#fff`"
v-bind="{ ...props }"
:color="color"
:style="style"
@click="() => emits('click')"
>
<slot></slot>
<slot name="icon"></slot>
<slot name="loading"></slot>
</ElButton>
</template>

View File

@ -1,5 +1,5 @@
<script lang="tsx">
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
import { ElCollapseTransition, ElTooltip, ElRow, ElCol } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { ref, unref, PropType, computed, defineComponent } from 'vue'
@ -16,6 +16,8 @@ const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('descriptions')
const defaultData = '-'
export default defineComponent({
name: 'Descriptions',
props: {
@ -36,7 +38,7 @@ export default defineComponent({
default: () => ({})
}
},
setup(props, { slots, attrs }) {
setup(props, { attrs }) {
const getBindValue = computed((): any => {
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
const obj = { ...attrs, ...props }
@ -59,7 +61,10 @@ export default defineComponent({
delete obj[key]
}
}
return obj
return {
labelClassName: `${prefixCls}-label`,
...obj
}
}
//
@ -103,26 +108,51 @@ export default defineComponent({
<ElCollapseTransition>
<div v-show={unref(show)} class={[`${prefixCls}-content`, 'p-20px']}>
<ElDescriptions {...unref(getBindValue)}>
{{
extra: () => (slots['extra'] ? slots['extra']() : props.extra),
default: () => {
return props.schema.map((item) => {
return (
<ElDescriptionsItem key={item.field} {...getBindItemValue(item)}>
{{
label: () => (item.slots?.label ? item.slots?.label(item) : item.label),
default: () =>
item.slots?.default
? item.slots?.default(props.data)
: get(props.data, item.field)
}}
</ElDescriptionsItem>
)
})
}
}}
</ElDescriptions>
<ElRow
gutter={0}
{...unref(getBindValue)}
class="outline-1px outline-[var(--el-border-color-lighter)] outline-solid"
>
{props.schema.map((item) => {
return (
<ElCol
key={item.field}
span={item.span || 24 / props.column}
class="flex items-stretch"
>
{props.direction === 'horizontal' ? (
<div class="flex items-stretch bg-[var(--el-fill-color-light)] outline-1px outline-[var(--el-border-color-lighter)] outline-solid flex-1">
<div
{...getBindItemValue(item)}
class="w-120px text-left px-8px py-11px font-700 color-[var(--el-text-color-regular)] border-r-1px border-r-[var(--el-border-color-lighter)] border-r-solid "
>
{item.label}
</div>
<div class="flex-1 px-8px py-11px bg-[var(--el-bg-color)] color-[var(--el-text-color-primary)] text-size-14px">
{item.slots?.default
? item.slots?.default(props.data)
: get(props.data, item.field) ?? defaultData}
</div>
</div>
) : (
<div class="bg-[var(--el-fill-color-light)] outline-1px outline-[var(--el-border-color-lighter)] outline-solid flex-1">
<div
{...getBindItemValue(item)}
class="text-left px-8px py-11px font-700 color-[var(--el-text-color-regular)] border-b-1px border-b-[var(--el-border-color-lighter)] border-b-solid"
>
{item.label}
</div>
<div class="flex-1 px-8px py-11px bg-[var(--el-bg-color)] color-[var(--el-text-color-primary)] text-size-14px">
{item.slots?.default
? item.slots?.default(props.data)
: get(props.data, item.field) ?? defaultData}
</div>
</div>
)}
</ElCol>
)
})}
</ElRow>
</div>
</ElCollapseTransition>
</div>
@ -153,9 +183,13 @@ export default defineComponent({
}
}
.@{prefix-cls}-content {
:deep(.@{elNamespace}-descriptions__cell) {
width: 0;
}
:deep(.@{prefix-cls}-label) {
width: 150px !important;
}
// .@{prefix-cls}-content {
// :deep(.@{elNamespace}-descriptions__cell) {
// width: 0;
// }
// }
</style>

View File

@ -10,7 +10,6 @@ const props = defineProps({
modelValue: propTypes.bool.def(false),
title: propTypes.string.def('Dialog'),
fullscreen: propTypes.bool.def(true),
top: propTypes.string.def('8vh'),
height: propTypes.oneOfType([String, Number]).def('500px'),
width: propTypes.oneOfType([String, Number]).def('700px')
@ -109,26 +108,30 @@ const dialogStyle = computed(() => {
</template>
<style lang="less">
// .@{elNamespace}-overlay-dialog {
// display: flex;
// justify-content: center;
// align-items: center;
// }
.@{elNamespace}-overlay-dialog {
display: flex;
justify-content: center;
align-items: center;
}
.@{elNamespace}-dialog {
// margin: 0 !important;
margin: 0 !important;
&__header {
height: 54px;
padding: 0;
margin-right: 0 !important;
border-bottom: 1px solid var(--el-border-color);
padding: 0;
height: 54px;
}
&__body {
padding: 15px !important;
}
&__footer {
border-top: 1px solid var(--el-border-color);
}
&__headerbtn {
top: 0;
}

View File

@ -4,7 +4,6 @@ import networkError from '@/assets/svgs/500.svg'
import noPermission from '@/assets/svgs/403.svg'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from '@/hooks/web/useI18n'
import { ElButton } from 'element-plus'
interface ErrorMap {
url: string
@ -51,7 +50,7 @@ const btnClick = () => {
<img width="350" :src="errorMap[type].url" alt="" />
<div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
<div class="mt-20px">
<ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
<BaseButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</BaseButton>
</div>
</div>
</div>

View File

@ -9,14 +9,14 @@ const prefixCls = getPrefixCls('footer')
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
const footerContent = computed(() => appStore.getFooterContent)
</script>
<template>
<div
:class="prefixCls"
class="text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-content-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]"
class="shrink-0 text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-content-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]"
>
Copyright ©2021-present {{ title }}
{{ footerContent }}
</div>
</template>

View File

@ -95,9 +95,6 @@ export default defineComponent({
// element form
const elFormRef = ref<ComponentRef<typeof ElForm>>()
// useFormprops
const outsideProps = ref<FormProps>({})
const mergeProps = ref<FormProps>({})
const getProps = computed(() => {
@ -155,8 +152,6 @@ export default defineComponent({
const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
// @ts-ignore
outsideProps.value = props
}
const delSchema = (field: string) => {
@ -364,13 +359,31 @@ export default defineComponent({
}
})
return (
return item.component === ComponentNameEnum.UPLOAD ? (
<Com
vModel:file-list={itemVal.value}
ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={
item.componentProps?.style || {
width: '100%'
}
}
>
{{ ...slotsMap }}
</Com>
) : (
<Com
vModel={itemVal.value}
ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={item.componentProps?.style || {}}
style={
item.componentProps?.style || {
width: '100%'
}
}
>
{{ ...slotsMap }}
</Com>
@ -447,6 +460,10 @@ export default defineComponent({
{...getFormBindValue()}
model={unref(getProps).isCustom ? unref(getProps).model : formModel}
class={prefixCls}
// @ts-ignore
onSubmit={(e: Event) => {
e.preventDefault()
}}
>
{{
//
@ -466,4 +483,16 @@ export default defineComponent({
margin-right: 0 !important;
margin-left: 0 !important;
}
.@{elNamespace}-form--inline {
:deep(.el-form-item__content) {
& > :first-child {
min-width: 229.5px;
}
}
.@{elNamespace}-input-number {
// 229.5pxel-input-number,
min-width: 229.5px;
}
}
</style>

View File

@ -25,6 +25,7 @@ import { Editor } from '@/components/Editor'
import { Text } from '@/components/Text'
import { JsonEditor } from '@/components/JsonEditor'
import { ComponentName } from '../types'
import { IconPicker } from '@/components/IconPicker'
const componentMap: Recordable<Component, ComponentName> = {
RadioGroup: ElRadioGroup,
@ -51,6 +52,7 @@ const componentMap: Recordable<Component, ComponentName> = {
TreeSelect: ElTreeSelect,
Upload: ElUpload,
JsonEditor: JsonEditor,
IconPicker: IconPicker,
Text: Text
}

View File

@ -56,6 +56,7 @@ export enum ComponentNameEnum {
TREE_SELECT = 'TreeSelect',
UPLOAD = 'Upload',
JSON_EDITOR = 'JsonEditor',
ICON_PICKER = 'IconPicker',
Text = 'Text'
}

View File

@ -25,6 +25,11 @@ const symbolId = computed(() => {
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
})
// 使线
const isUseOnline = computed(() => {
return import.meta.env.VITE_USE_ONLINE_ICON === 'true'
})
const getIconifyStyle = computed(() => {
const { color, size } = props
return {
@ -40,7 +45,10 @@ const getIconifyStyle = computed(() => {
<use :xlink:href="symbolId" />
</svg>
<Icon v-else :icon="icon" :style="getIconifyStyle" />
<template v-else>
<Icon v-if="isUseOnline" :icon="icon" :style="getIconifyStyle" />
<div v-else :class="`${icon} iconify`" :style="getIconifyStyle"></div>
</template>
</ElIcon>
</template>
@ -49,11 +57,18 @@ const getIconifyStyle = computed(() => {
.@{prefix-cls},
.iconify {
&:hover {
:deep(svg) {
:deep(svg) {
&:hover {
// stylelint-disable-next-line
color: v-bind(hoverColor) !important;
}
}
}
.iconify {
&:hover {
// stylelint-disable-next-line
color: v-bind(hoverColor) !important;
}
}
</style>

View File

@ -0,0 +1,3 @@
import IconPicker from './src/IconPicker.vue'
export { IconPicker }

View File

@ -0,0 +1,193 @@
<script setup lang="ts">
import epIcons from './data/icons.ep'
import antIcons from './data/icons.ant-design'
import tIcons from './data/icons.tdesign'
import { useDesign } from '@/hooks/web/useDesign'
import { ElInput, ElPopover, ElScrollbar, ElTabs, ElTabPane, ElPagination } from 'element-plus'
import { useAppStore } from '@/store/modules/app'
import { computed, CSSProperties, ref, unref, watch } from 'vue'
import { nextTick } from 'vue'
const init = async (icon?: string) => {
if (!icon) return
const iconInfo = icon.split(':')
iconName.value = iconInfo[0]
const wrapIndex = icons.findIndex((item) => item.prefix === iconInfo[0])
// icon
const index = filterItemIcons(icons[wrapIndex].icons).findIndex((item) => item === icon)
// icon
await nextTick()
currentPage.value = Math.ceil((index + 1) / unref(pageSize))
}
const modelValue = defineModel<string>()
const appStore = useAppStore()
const size = computed(() => appStore.getCurrentSize)
const iconSize = computed(() => {
return unref(size) === 'small'
? 'var(--el-component-size-small)'
: unref(size) === 'large'
? 'var(--el-component-size-large)'
: 'var(--el-component-size)'
})
const iconWrapStyle = computed((): CSSProperties => {
return {
width: unref(iconSize),
height: unref(iconSize),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset',
position: 'relative',
left: '-1px',
cursor: 'pointer'
}
})
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('icon-picker')
const icons = [epIcons, antIcons, tIcons]
const iconName = ref(icons[0].prefix)
const currentIconNameIndex = computed(() => {
return icons.findIndex((item) => item.prefix === unref(iconName))
})
const tabChange = () => {
currentPage.value = 1
}
const pageSize = ref(49)
const currentPage = ref(1)
const filterIcons = (icons: string[]) => {
const start = (unref(currentPage) - 1) * unref(pageSize)
const end = unref(currentPage) * unref(pageSize)
return icons.slice(start, end)
}
watch(
() => modelValue.value,
async (val) => {
await nextTick()
val && init(val)
},
{
immediate: true
}
)
const popoverShow = () => {
init(unref(modelValue))
}
const iconSelect = (icon: string) => {
// icon
if (icon === unref(modelValue)) {
modelValue.value = ''
return
}
modelValue.value = icon
}
const search = ref('')
const filterItemIcons = (icons: string[]) => {
return icons.filter((item) => item.includes(unref(search)))
}
const inputClear = () => {
init(unref(modelValue))
}
</script>
<template>
<div :class="prefixCls" class="flex justify-center items-center box">
<ElInput disabled v-model="modelValue" clearable />
<ElPopover
placement="bottom"
trigger="click"
:width="450"
popper-style="box-shadow: rgb(14 18 22 / 35%) 0px 10px 38px -10px, rgb(14 18 22 / 20%) 0px 10px 20px -15px; height: 380px;"
@show="popoverShow"
>
<template #reference>
<div :style="iconWrapStyle">
<Icon v-if="modelValue" :icon="modelValue" />
</div>
</template>
<ElScrollbar class="h-[calc(100%-50px)]!">
<ElInput
v-model="search"
class="mb-20px"
clearable
placeholder="搜索图标"
@clear="inputClear"
/>
<ElTabs tab-position="left" v-model="iconName" @tab-change="tabChange">
<ElTabPane v-for="item in icons" :key="item.name" :label="item.name" :name="item.prefix">
<div class="flex flex-wrap box-border">
<div
v-for="icon in filterIcons(filterItemIcons(item.icons))"
:key="icon"
:style="{
width: iconSize,
height: iconSize,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
border: `1px solid ${
icon === modelValue ? 'var(--el-color-primary)' : 'var(--el-border-color)'
}`,
boxSizing: 'border-box',
margin: '2px',
transition: 'all 0.3s'
}"
class="hover:border-color-[var(--el-color-primary)]!"
@click="iconSelect(icon)"
>
<Icon
:icon="icon"
:color="icon === modelValue ? 'var(--el-color-primary)' : 'inherit'"
/>
</div>
</div>
</ElTabPane>
</ElTabs>
</ElScrollbar>
<div
class="h-50px absolute bottom-0 left-0 flex items-center pl-[var(--el-popover-padding)] pr-[var(--el-popover-padding)]"
>
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:pager-count="5"
small
:page-sizes="[100, 200, 300, 400]"
layout="total, prev, pager, next, jumper"
:total="filterItemIcons(icons[currentIconNameIndex].icons).length"
/>
</div>
</ElPopover>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-icon-picker';
.@{prefix-cls} {
:deep(.@{elNamespace}-input__wrapper) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
</style>

View File

@ -0,0 +1,795 @@
export default {
name: 'Ant Design Icons',
prefix: 'ant-design',
icons: [
'ant-design:account-book-filled',
'ant-design:account-book-outlined',
'ant-design:account-book-twotone',
'ant-design:aim-outlined',
'ant-design:alert-filled',
'ant-design:alert-outlined',
'ant-design:alert-twotone',
'ant-design:alibaba-outlined',
'ant-design:align-center-outlined',
'ant-design:align-left-outlined',
'ant-design:align-right-outlined',
'ant-design:alipay-circle-filled',
'ant-design:alipay-circle-outlined',
'ant-design:alipay-outlined',
'ant-design:alipay-square-filled',
'ant-design:aliwangwang-filled',
'ant-design:aliwangwang-outlined',
'ant-design:aliyun-outlined',
'ant-design:amazon-circle-filled',
'ant-design:amazon-outlined',
'ant-design:amazon-square-filled',
'ant-design:android-filled',
'ant-design:android-outlined',
'ant-design:ant-cloud-outlined',
'ant-design:ant-design-outlined',
'ant-design:apartment-outlined',
'ant-design:api-filled',
'ant-design:api-outlined',
'ant-design:api-twotone',
'ant-design:apple-filled',
'ant-design:apple-outlined',
'ant-design:appstore-add-outlined',
'ant-design:appstore-filled',
'ant-design:appstore-outlined',
'ant-design:appstore-twotone',
'ant-design:area-chart-outlined',
'ant-design:arrow-down-outlined',
'ant-design:arrow-left-outlined',
'ant-design:arrow-right-outlined',
'ant-design:arrow-up-outlined',
'ant-design:arrows-alt-outlined',
'ant-design:audio-filled',
'ant-design:audio-muted-outlined',
'ant-design:audio-outlined',
'ant-design:audio-twotone',
'ant-design:audit-outlined',
'ant-design:backward-filled',
'ant-design:backward-outlined',
'ant-design:bank-filled',
'ant-design:bank-outlined',
'ant-design:bank-twotone',
'ant-design:bar-chart-outlined',
'ant-design:barcode-outlined',
'ant-design:bars-outlined',
'ant-design:behance-circle-filled',
'ant-design:behance-outlined',
'ant-design:behance-square-filled',
'ant-design:behance-square-outlined',
'ant-design:bell-filled',
'ant-design:bell-outlined',
'ant-design:bell-twotone',
'ant-design:bg-colors-outlined',
'ant-design:block-outlined',
'ant-design:bold-outlined',
'ant-design:book-filled',
'ant-design:book-outlined',
'ant-design:book-twotone',
'ant-design:border-bottom-outlined',
'ant-design:border-horizontal-outlined',
'ant-design:border-inner-outlined',
'ant-design:border-left-outlined',
'ant-design:border-outer-outlined',
'ant-design:border-outlined',
'ant-design:border-right-outlined',
'ant-design:border-top-outlined',
'ant-design:border-verticle-outlined',
'ant-design:borderless-table-outlined',
'ant-design:box-plot-filled',
'ant-design:box-plot-outlined',
'ant-design:box-plot-twotone',
'ant-design:branches-outlined',
'ant-design:bug-filled',
'ant-design:bug-outlined',
'ant-design:bug-twotone',
'ant-design:build-filled',
'ant-design:build-outlined',
'ant-design:build-twotone',
'ant-design:bulb-filled',
'ant-design:bulb-outlined',
'ant-design:bulb-twotone',
'ant-design:calculator-filled',
'ant-design:calculator-outlined',
'ant-design:calculator-twotone',
'ant-design:calendar-filled',
'ant-design:calendar-outlined',
'ant-design:calendar-twotone',
'ant-design:camera-filled',
'ant-design:camera-outlined',
'ant-design:camera-twotone',
'ant-design:car-filled',
'ant-design:car-outlined',
'ant-design:car-twotone',
'ant-design:caret-down-filled',
'ant-design:caret-down-outlined',
'ant-design:caret-left-filled',
'ant-design:caret-left-outlined',
'ant-design:caret-right-filled',
'ant-design:caret-right-outlined',
'ant-design:caret-up-filled',
'ant-design:caret-up-outlined',
'ant-design:carry-out-filled',
'ant-design:carry-out-outlined',
'ant-design:carry-out-twotone',
'ant-design:check-circle-filled',
'ant-design:check-circle-outlined',
'ant-design:check-circle-twotone',
'ant-design:check-outlined',
'ant-design:check-square-filled',
'ant-design:check-square-outlined',
'ant-design:check-square-twotone',
'ant-design:chrome-filled',
'ant-design:chrome-outlined',
'ant-design:ci-circle-filled',
'ant-design:ci-circle-outlined',
'ant-design:ci-circle-twotone',
'ant-design:ci-outlined',
'ant-design:ci-twotone',
'ant-design:clear-outlined',
'ant-design:clock-circle-filled',
'ant-design:clock-circle-outlined',
'ant-design:clock-circle-twotone',
'ant-design:close-circle-filled',
'ant-design:close-circle-outlined',
'ant-design:close-circle-twotone',
'ant-design:close-outlined',
'ant-design:close-square-filled',
'ant-design:close-square-outlined',
'ant-design:close-square-twotone',
'ant-design:cloud-download-outlined',
'ant-design:cloud-filled',
'ant-design:cloud-outlined',
'ant-design:cloud-server-outlined',
'ant-design:cloud-sync-outlined',
'ant-design:cloud-twotone',
'ant-design:cloud-upload-outlined',
'ant-design:cluster-outlined',
'ant-design:code-filled',
'ant-design:code-outlined',
'ant-design:code-sandbox-circle-filled',
'ant-design:code-sandbox-outlined',
'ant-design:code-sandbox-square-filled',
'ant-design:code-twotone',
'ant-design:codepen-circle-filled',
'ant-design:codepen-circle-outlined',
'ant-design:codepen-outlined',
'ant-design:codepen-square-filled',
'ant-design:coffee-outlined',
'ant-design:column-height-outlined',
'ant-design:column-width-outlined',
'ant-design:comment-outlined',
'ant-design:compass-filled',
'ant-design:compass-outlined',
'ant-design:compass-twotone',
'ant-design:compress-outlined',
'ant-design:console-sql-outlined',
'ant-design:contacts-filled',
'ant-design:contacts-outlined',
'ant-design:contacts-twotone',
'ant-design:container-filled',
'ant-design:container-outlined',
'ant-design:container-twotone',
'ant-design:control-filled',
'ant-design:control-outlined',
'ant-design:control-twotone',
'ant-design:copy-filled',
'ant-design:copy-outlined',
'ant-design:copy-twotone',
'ant-design:copyright-circle-filled',
'ant-design:copyright-circle-outlined',
'ant-design:copyright-circle-twotone',
'ant-design:copyright-outlined',
'ant-design:copyright-twotone',
'ant-design:credit-card-filled',
'ant-design:credit-card-outlined',
'ant-design:credit-card-twotone',
'ant-design:crown-filled',
'ant-design:crown-outlined',
'ant-design:crown-twotone',
'ant-design:customer-service-filled',
'ant-design:customer-service-outlined',
'ant-design:customer-service-twotone',
'ant-design:dash-outlined',
'ant-design:dashboard-filled',
'ant-design:dashboard-outlined',
'ant-design:dashboard-twotone',
'ant-design:database-filled',
'ant-design:database-outlined',
'ant-design:database-twotone',
'ant-design:delete-column-outlined',
'ant-design:delete-filled',
'ant-design:delete-outlined',
'ant-design:delete-row-outlined',
'ant-design:delete-twotone',
'ant-design:delivered-procedure-outlined',
'ant-design:deployment-unit-outlined',
'ant-design:desktop-outlined',
'ant-design:diff-filled',
'ant-design:diff-outlined',
'ant-design:diff-twotone',
'ant-design:dingding-outlined',
'ant-design:dingtalk-circle-filled',
'ant-design:dingtalk-outlined',
'ant-design:dingtalk-square-filled',
'ant-design:disconnect-outlined',
'ant-design:dislike-filled',
'ant-design:dislike-outlined',
'ant-design:dislike-twotone',
'ant-design:dollar-circle-filled',
'ant-design:dollar-circle-outlined',
'ant-design:dollar-circle-twotone',
'ant-design:dollar-outlined',
'ant-design:dollar-twotone',
'ant-design:dot-chart-outlined',
'ant-design:double-left-outlined',
'ant-design:double-right-outlined',
'ant-design:down-circle-filled',
'ant-design:down-circle-outlined',
'ant-design:down-circle-twotone',
'ant-design:down-outlined',
'ant-design:down-square-filled',
'ant-design:down-square-outlined',
'ant-design:down-square-twotone',
'ant-design:download-outlined',
'ant-design:drag-outlined',
'ant-design:dribbble-circle-filled',
'ant-design:dribbble-outlined',
'ant-design:dribbble-square-filled',
'ant-design:dribbble-square-outlined',
'ant-design:dropbox-circle-filled',
'ant-design:dropbox-outlined',
'ant-design:dropbox-square-filled',
'ant-design:edit-filled',
'ant-design:edit-outlined',
'ant-design:edit-twotone',
'ant-design:ellipsis-outlined',
'ant-design:enter-outlined',
'ant-design:environment-filled',
'ant-design:environment-outlined',
'ant-design:environment-twotone',
'ant-design:euro-circle-filled',
'ant-design:euro-circle-outlined',
'ant-design:euro-circle-twotone',
'ant-design:euro-outlined',
'ant-design:euro-twotone',
'ant-design:exception-outlined',
'ant-design:exclamation-circle-filled',
'ant-design:exclamation-circle-outlined',
'ant-design:exclamation-circle-twotone',
'ant-design:exclamation-outlined',
'ant-design:expand-alt-outlined',
'ant-design:expand-outlined',
'ant-design:experiment-filled',
'ant-design:experiment-outlined',
'ant-design:experiment-twotone',
'ant-design:export-outlined',
'ant-design:eye-filled',
'ant-design:eye-invisible-filled',
'ant-design:eye-invisible-outlined',
'ant-design:eye-invisible-twotone',
'ant-design:eye-outlined',
'ant-design:eye-twotone',
'ant-design:facebook-filled',
'ant-design:facebook-outlined',
'ant-design:fall-outlined',
'ant-design:fast-backward-filled',
'ant-design:fast-backward-outlined',
'ant-design:fast-forward-filled',
'ant-design:fast-forward-outlined',
'ant-design:field-binary-outlined',
'ant-design:field-number-outlined',
'ant-design:field-string-outlined',
'ant-design:field-time-outlined',
'ant-design:file-add-filled',
'ant-design:file-add-outlined',
'ant-design:file-add-twotone',
'ant-design:file-done-outlined',
'ant-design:file-excel-filled',
'ant-design:file-excel-outlined',
'ant-design:file-excel-twotone',
'ant-design:file-exclamation-filled',
'ant-design:file-exclamation-outlined',
'ant-design:file-exclamation-twotone',
'ant-design:file-filled',
'ant-design:file-gif-outlined',
'ant-design:file-image-filled',
'ant-design:file-image-outlined',
'ant-design:file-image-twotone',
'ant-design:file-jpg-outlined',
'ant-design:file-markdown-filled',
'ant-design:file-markdown-outlined',
'ant-design:file-markdown-twotone',
'ant-design:file-outlined',
'ant-design:file-pdf-filled',
'ant-design:file-pdf-outlined',
'ant-design:file-pdf-twotone',
'ant-design:file-ppt-filled',
'ant-design:file-ppt-outlined',
'ant-design:file-ppt-twotone',
'ant-design:file-protect-outlined',
'ant-design:file-search-outlined',
'ant-design:file-sync-outlined',
'ant-design:file-text-filled',
'ant-design:file-text-outlined',
'ant-design:file-text-twotone',
'ant-design:file-twotone',
'ant-design:file-unknown-filled',
'ant-design:file-unknown-outlined',
'ant-design:file-unknown-twotone',
'ant-design:file-word-filled',
'ant-design:file-word-outlined',
'ant-design:file-word-twotone',
'ant-design:file-zip-filled',
'ant-design:file-zip-outlined',
'ant-design:file-zip-twotone',
'ant-design:filter-filled',
'ant-design:filter-outlined',
'ant-design:filter-twotone',
'ant-design:fire-filled',
'ant-design:fire-outlined',
'ant-design:fire-twotone',
'ant-design:flag-filled',
'ant-design:flag-outlined',
'ant-design:flag-twotone',
'ant-design:folder-add-filled',
'ant-design:folder-add-outlined',
'ant-design:folder-add-twotone',
'ant-design:folder-filled',
'ant-design:folder-open-filled',
'ant-design:folder-open-outlined',
'ant-design:folder-open-twotone',
'ant-design:folder-outlined',
'ant-design:folder-twotone',
'ant-design:folder-view-outlined',
'ant-design:font-colors-outlined',
'ant-design:font-size-outlined',
'ant-design:fork-outlined',
'ant-design:form-outlined',
'ant-design:format-painter-filled',
'ant-design:format-painter-outlined',
'ant-design:forward-filled',
'ant-design:forward-outlined',
'ant-design:frown-filled',
'ant-design:frown-outlined',
'ant-design:frown-twotone',
'ant-design:fullscreen-exit-outlined',
'ant-design:fullscreen-outlined',
'ant-design:function-outlined',
'ant-design:fund-filled',
'ant-design:fund-outlined',
'ant-design:fund-projection-screen-outlined',
'ant-design:fund-twotone',
'ant-design:fund-view-outlined',
'ant-design:funnel-plot-filled',
'ant-design:funnel-plot-outlined',
'ant-design:funnel-plot-twotone',
'ant-design:gateway-outlined',
'ant-design:gif-outlined',
'ant-design:gift-filled',
'ant-design:gift-outlined',
'ant-design:gift-twotone',
'ant-design:github-filled',
'ant-design:github-outlined',
'ant-design:gitlab-filled',
'ant-design:gitlab-outlined',
'ant-design:global-outlined',
'ant-design:gold-filled',
'ant-design:gold-outlined',
'ant-design:gold-twotone',
'ant-design:golden-filled',
'ant-design:google-circle-filled',
'ant-design:google-outlined',
'ant-design:google-plus-circle-filled',
'ant-design:google-plus-outlined',
'ant-design:google-plus-square-filled',
'ant-design:google-square-filled',
'ant-design:group-outlined',
'ant-design:hdd-filled',
'ant-design:hdd-outlined',
'ant-design:hdd-twotone',
'ant-design:heart-filled',
'ant-design:heart-outlined',
'ant-design:heart-twotone',
'ant-design:heat-map-outlined',
'ant-design:highlight-filled',
'ant-design:highlight-outlined',
'ant-design:highlight-twotone',
'ant-design:history-outlined',
'ant-design:holder-outlined',
'ant-design:home-filled',
'ant-design:home-outlined',
'ant-design:home-twotone',
'ant-design:hourglass-filled',
'ant-design:hourglass-outlined',
'ant-design:hourglass-twotone',
'ant-design:html5-filled',
'ant-design:html5-outlined',
'ant-design:html5-twotone',
'ant-design:idcard-filled',
'ant-design:idcard-outlined',
'ant-design:idcard-twotone',
'ant-design:ie-circle-filled',
'ant-design:ie-outlined',
'ant-design:ie-square-filled',
'ant-design:import-outlined',
'ant-design:inbox-outlined',
'ant-design:info-circle-filled',
'ant-design:info-circle-outlined',
'ant-design:info-circle-twotone',
'ant-design:info-outlined',
'ant-design:insert-row-above-outlined',
'ant-design:insert-row-below-outlined',
'ant-design:insert-row-left-outlined',
'ant-design:insert-row-right-outlined',
'ant-design:instagram-filled',
'ant-design:instagram-outlined',
'ant-design:insurance-filled',
'ant-design:insurance-outlined',
'ant-design:insurance-twotone',
'ant-design:interaction-filled',
'ant-design:interaction-outlined',
'ant-design:interaction-twotone',
'ant-design:issues-close-outlined',
'ant-design:italic-outlined',
'ant-design:key-outlined',
'ant-design:laptop-outlined',
'ant-design:layout-filled',
'ant-design:layout-outlined',
'ant-design:layout-twotone',
'ant-design:left-circle-filled',
'ant-design:left-circle-outlined',
'ant-design:left-circle-twotone',
'ant-design:left-outlined',
'ant-design:left-square-filled',
'ant-design:left-square-outlined',
'ant-design:left-square-twotone',
'ant-design:like-filled',
'ant-design:like-outlined',
'ant-design:like-twotone',
'ant-design:line-chart-outlined',
'ant-design:line-height-outlined',
'ant-design:line-outlined',
'ant-design:link-outlined',
'ant-design:linkedin-filled',
'ant-design:linkedin-outlined',
'ant-design:loading-3-quarters-outlined',
'ant-design:loading-outlined',
'ant-design:lock-filled',
'ant-design:lock-outlined',
'ant-design:lock-twotone',
'ant-design:login-outlined',
'ant-design:logout-outlined',
'ant-design:mac-command-filled',
'ant-design:mac-command-outlined',
'ant-design:mail-filled',
'ant-design:mail-outlined',
'ant-design:mail-twotone',
'ant-design:man-outlined',
'ant-design:medicine-box-filled',
'ant-design:medicine-box-outlined',
'ant-design:medicine-box-twotone',
'ant-design:medium-circle-filled',
'ant-design:medium-outlined',
'ant-design:medium-square-filled',
'ant-design:medium-workmark-outlined',
'ant-design:meh-filled',
'ant-design:meh-outlined',
'ant-design:meh-twotone',
'ant-design:menu-fold-outlined',
'ant-design:menu-outlined',
'ant-design:menu-unfold-outlined',
'ant-design:merge-cells-outlined',
'ant-design:message-filled',
'ant-design:message-outlined',
'ant-design:message-twotone',
'ant-design:minus-circle-filled',
'ant-design:minus-circle-outlined',
'ant-design:minus-circle-twotone',
'ant-design:minus-outlined',
'ant-design:minus-square-filled',
'ant-design:minus-square-outlined',
'ant-design:minus-square-twotone',
'ant-design:mobile-filled',
'ant-design:mobile-outlined',
'ant-design:mobile-twotone',
'ant-design:money-collect-filled',
'ant-design:money-collect-outlined',
'ant-design:money-collect-twotone',
'ant-design:monitor-outlined',
'ant-design:more-outlined',
'ant-design:node-collapse-outlined',
'ant-design:node-expand-outlined',
'ant-design:node-index-outlined',
'ant-design:notification-filled',
'ant-design:notification-outlined',
'ant-design:notification-twotone',
'ant-design:number-outlined',
'ant-design:one-to-one-outlined',
'ant-design:ordered-list-outlined',
'ant-design:paper-clip-outlined',
'ant-design:partition-outlined',
'ant-design:pause-circle-filled',
'ant-design:pause-circle-outlined',
'ant-design:pause-circle-twotone',
'ant-design:pause-outlined',
'ant-design:pay-circle-filled',
'ant-design:pay-circle-outlined',
'ant-design:percentage-outlined',
'ant-design:phone-filled',
'ant-design:phone-outlined',
'ant-design:phone-twotone',
'ant-design:pic-center-outlined',
'ant-design:pic-left-outlined',
'ant-design:pic-right-outlined',
'ant-design:picture-filled',
'ant-design:picture-outlined',
'ant-design:picture-twotone',
'ant-design:pie-chart-filled',
'ant-design:pie-chart-outlined',
'ant-design:pie-chart-twotone',
'ant-design:play-circle-filled',
'ant-design:play-circle-outlined',
'ant-design:play-circle-twotone',
'ant-design:play-square-filled',
'ant-design:play-square-outlined',
'ant-design:play-square-twotone',
'ant-design:plus-circle-filled',
'ant-design:plus-circle-outlined',
'ant-design:plus-circle-twotone',
'ant-design:plus-outlined',
'ant-design:plus-square-filled',
'ant-design:plus-square-outlined',
'ant-design:plus-square-twotone',
'ant-design:pound-circle-filled',
'ant-design:pound-circle-outlined',
'ant-design:pound-circle-twotone',
'ant-design:pound-outlined',
'ant-design:poweroff-outlined',
'ant-design:printer-filled',
'ant-design:printer-outlined',
'ant-design:printer-twotone',
'ant-design:profile-filled',
'ant-design:profile-outlined',
'ant-design:profile-twotone',
'ant-design:project-filled',
'ant-design:project-outlined',
'ant-design:project-twotone',
'ant-design:property-safety-filled',
'ant-design:property-safety-outlined',
'ant-design:property-safety-twotone',
'ant-design:pull-request-outlined',
'ant-design:pushpin-filled',
'ant-design:pushpin-outlined',
'ant-design:pushpin-twotone',
'ant-design:qq-circle-filled',
'ant-design:qq-outlined',
'ant-design:qq-square-filled',
'ant-design:qrcode-outlined',
'ant-design:question-circle-filled',
'ant-design:question-circle-outlined',
'ant-design:question-circle-twotone',
'ant-design:question-outlined',
'ant-design:radar-chart-outlined',
'ant-design:radius-bottomleft-outlined',
'ant-design:radius-bottomright-outlined',
'ant-design:radius-setting-outlined',
'ant-design:radius-upleft-outlined',
'ant-design:radius-upright-outlined',
'ant-design:read-filled',
'ant-design:read-outlined',
'ant-design:reconciliation-filled',
'ant-design:reconciliation-outlined',
'ant-design:reconciliation-twotone',
'ant-design:red-envelope-filled',
'ant-design:red-envelope-outlined',
'ant-design:red-envelope-twotone',
'ant-design:reddit-circle-filled',
'ant-design:reddit-outlined',
'ant-design:reddit-square-filled',
'ant-design:redo-outlined',
'ant-design:reload-outlined',
'ant-design:rest-filled',
'ant-design:rest-outlined',
'ant-design:rest-twotone',
'ant-design:retweet-outlined',
'ant-design:right-circle-filled',
'ant-design:right-circle-outlined',
'ant-design:right-circle-twotone',
'ant-design:right-outlined',
'ant-design:right-square-filled',
'ant-design:right-square-outlined',
'ant-design:right-square-twotone',
'ant-design:rise-outlined',
'ant-design:robot-filled',
'ant-design:robot-outlined',
'ant-design:rocket-filled',
'ant-design:rocket-outlined',
'ant-design:rocket-twotone',
'ant-design:rollback-outlined',
'ant-design:rotate-left-outlined',
'ant-design:rotate-right-outlined',
'ant-design:safety-certificate-filled',
'ant-design:safety-certificate-outlined',
'ant-design:safety-certificate-twotone',
'ant-design:safety-outlined',
'ant-design:save-filled',
'ant-design:save-outlined',
'ant-design:save-twotone',
'ant-design:scan-outlined',
'ant-design:schedule-filled',
'ant-design:schedule-outlined',
'ant-design:schedule-twotone',
'ant-design:scissor-outlined',
'ant-design:search-outlined',
'ant-design:security-scan-filled',
'ant-design:security-scan-outlined',
'ant-design:security-scan-twotone',
'ant-design:select-outlined',
'ant-design:send-outlined',
'ant-design:setting-filled',
'ant-design:setting-outlined',
'ant-design:setting-twotone',
'ant-design:shake-outlined',
'ant-design:share-alt-outlined',
'ant-design:shop-filled',
'ant-design:shop-outlined',
'ant-design:shop-twotone',
'ant-design:shopping-cart-outlined',
'ant-design:shopping-filled',
'ant-design:shopping-outlined',
'ant-design:shopping-twotone',
'ant-design:shrink-outlined',
'ant-design:signal-filled',
'ant-design:sisternode-outlined',
'ant-design:sketch-circle-filled',
'ant-design:sketch-outlined',
'ant-design:sketch-square-filled',
'ant-design:skin-filled',
'ant-design:skin-outlined',
'ant-design:skin-twotone',
'ant-design:skype-filled',
'ant-design:skype-outlined',
'ant-design:slack-circle-filled',
'ant-design:slack-outlined',
'ant-design:slack-square-filled',
'ant-design:slack-square-outlined',
'ant-design:sliders-filled',
'ant-design:sliders-outlined',
'ant-design:sliders-twotone',
'ant-design:small-dash-outlined',
'ant-design:smile-filled',
'ant-design:smile-outlined',
'ant-design:smile-twotone',
'ant-design:snippets-filled',
'ant-design:snippets-outlined',
'ant-design:snippets-twotone',
'ant-design:solution-outlined',
'ant-design:sort-ascending-outlined',
'ant-design:sort-descending-outlined',
'ant-design:sound-filled',
'ant-design:sound-outlined',
'ant-design:sound-twotone',
'ant-design:split-cells-outlined',
'ant-design:star-filled',
'ant-design:star-outlined',
'ant-design:star-twotone',
'ant-design:step-backward-filled',
'ant-design:step-backward-outlined',
'ant-design:step-forward-filled',
'ant-design:step-forward-outlined',
'ant-design:stock-outlined',
'ant-design:stop-filled',
'ant-design:stop-outlined',
'ant-design:stop-twotone',
'ant-design:strikethrough-outlined',
'ant-design:subnode-outlined',
'ant-design:swap-left-outlined',
'ant-design:swap-outlined',
'ant-design:swap-right-outlined',
'ant-design:switcher-filled',
'ant-design:switcher-outlined',
'ant-design:switcher-twotone',
'ant-design:sync-outlined',
'ant-design:table-outlined',
'ant-design:tablet-filled',
'ant-design:tablet-outlined',
'ant-design:tablet-twotone',
'ant-design:tag-filled',
'ant-design:tag-outlined',
'ant-design:tag-twotone',
'ant-design:tags-filled',
'ant-design:tags-outlined',
'ant-design:tags-twotone',
'ant-design:taobao-circle-filled',
'ant-design:taobao-circle-outlined',
'ant-design:taobao-outlined',
'ant-design:taobao-square-filled',
'ant-design:team-outlined',
'ant-design:thunderbolt-filled',
'ant-design:thunderbolt-outlined',
'ant-design:thunderbolt-twotone',
'ant-design:to-top-outlined',
'ant-design:tool-filled',
'ant-design:tool-outlined',
'ant-design:tool-twotone',
'ant-design:trademark-circle-filled',
'ant-design:trademark-circle-outlined',
'ant-design:trademark-circle-twotone',
'ant-design:trademark-outlined',
'ant-design:transaction-outlined',
'ant-design:translation-outlined',
'ant-design:trophy-filled',
'ant-design:trophy-outlined',
'ant-design:trophy-twotone',
'ant-design:twitter-circle-filled',
'ant-design:twitter-outlined',
'ant-design:twitter-square-filled',
'ant-design:underline-outlined',
'ant-design:undo-outlined',
'ant-design:ungroup-outlined',
'ant-design:unlock-filled',
'ant-design:unlock-outlined',
'ant-design:unlock-twotone',
'ant-design:unordered-list-outlined',
'ant-design:up-circle-filled',
'ant-design:up-circle-outlined',
'ant-design:up-circle-twotone',
'ant-design:up-outlined',
'ant-design:up-square-filled',
'ant-design:up-square-outlined',
'ant-design:up-square-twotone',
'ant-design:upload-outlined',
'ant-design:usb-filled',
'ant-design:usb-outlined',
'ant-design:usb-twotone',
'ant-design:user-add-outlined',
'ant-design:user-delete-outlined',
'ant-design:user-outlined',
'ant-design:user-switch-outlined',
'ant-design:usergroup-add-outlined',
'ant-design:usergroup-delete-outlined',
'ant-design:verified-outlined',
'ant-design:vertical-align-bottom-outlined',
'ant-design:vertical-align-middle-outlined',
'ant-design:vertical-align-top-outlined',
'ant-design:vertical-left-outlined',
'ant-design:vertical-right-outlined',
'ant-design:video-camera-add-outlined',
'ant-design:video-camera-filled',
'ant-design:video-camera-outlined',
'ant-design:video-camera-twotone',
'ant-design:wallet-filled',
'ant-design:wallet-outlined',
'ant-design:wallet-twotone',
'ant-design:warning-filled',
'ant-design:warning-outlined',
'ant-design:warning-twotone',
'ant-design:wechat-filled',
'ant-design:wechat-outlined',
'ant-design:weibo-circle-filled',
'ant-design:weibo-circle-outlined',
'ant-design:weibo-outlined',
'ant-design:weibo-square-filled',
'ant-design:weibo-square-outlined',
'ant-design:whats-app-outlined',
'ant-design:wifi-outlined',
'ant-design:windows-filled',
'ant-design:windows-outlined',
'ant-design:woman-outlined',
'ant-design:yahoo-filled',
'ant-design:yahoo-outlined',
'ant-design:youtube-filled',
'ant-design:youtube-outlined',
'ant-design:yuque-filled',
'ant-design:yuque-outlined',
'ant-design:zhihu-circle-filled',
'ant-design:zhihu-outlined',
'ant-design:zhihu-square-filled',
'ant-design:zoom-in-outlined',
'ant-design:zoom-out-outlined'
]
}

View File

@ -0,0 +1,299 @@
export default {
name: 'Element Plus',
prefix: 'ep',
icons: [
'ep:add-location',
'ep:aim',
'ep:alarm-clock',
'ep:apple',
'ep:arrow-down',
'ep:arrow-down-bold',
'ep:arrow-left',
'ep:arrow-left-bold',
'ep:arrow-right',
'ep:arrow-right-bold',
'ep:arrow-up',
'ep:arrow-up-bold',
'ep:avatar',
'ep:back',
'ep:baseball',
'ep:basketball',
'ep:bell',
'ep:bell-filled',
'ep:bicycle',
'ep:bottom',
'ep:bottom-left',
'ep:bottom-right',
'ep:bowl',
'ep:box',
'ep:briefcase',
'ep:brush',
'ep:brush-filled',
'ep:burger',
'ep:calendar',
'ep:camera',
'ep:camera-filled',
'ep:caret-bottom',
'ep:caret-left',
'ep:caret-right',
'ep:caret-top',
'ep:cellphone',
'ep:chat-dot-round',
'ep:chat-dot-square',
'ep:chat-line-round',
'ep:chat-line-square',
'ep:chat-round',
'ep:chat-square',
'ep:check',
'ep:checked',
'ep:cherry',
'ep:chicken',
'ep:chrome-filled',
'ep:circle-check',
'ep:circle-check-filled',
'ep:circle-close',
'ep:circle-close-filled',
'ep:circle-plus',
'ep:circle-plus-filled',
'ep:clock',
'ep:close',
'ep:close-bold',
'ep:cloudy',
'ep:coffee',
'ep:coffee-cup',
'ep:coin',
'ep:cold-drink',
'ep:collection',
'ep:collection-tag',
'ep:comment',
'ep:compass',
'ep:connection',
'ep:coordinate',
'ep:copy-document',
'ep:cpu',
'ep:credit-card',
'ep:crop',
'ep:d-arrow-left',
'ep:d-arrow-right',
'ep:d-caret',
'ep:data-analysis',
'ep:data-board',
'ep:data-line',
'ep:delete',
'ep:delete-filled',
'ep:delete-location',
'ep:dessert',
'ep:discount',
'ep:dish',
'ep:dish-dot',
'ep:document',
'ep:document-add',
'ep:document-checked',
'ep:document-copy',
'ep:document-delete',
'ep:document-remove',
'ep:download',
'ep:drizzling',
'ep:edit',
'ep:edit-pen',
'ep:eleme',
'ep:eleme-filled',
'ep:element-plus',
'ep:expand',
'ep:failed',
'ep:female',
'ep:files',
'ep:film',
'ep:filter',
'ep:finished',
'ep:first-aid-kit',
'ep:flag',
'ep:fold',
'ep:folder',
'ep:folder-add',
'ep:folder-checked',
'ep:folder-delete',
'ep:folder-opened',
'ep:folder-remove',
'ep:food',
'ep:football',
'ep:fork-spoon',
'ep:fries',
'ep:full-screen',
'ep:goblet',
'ep:goblet-full',
'ep:goblet-square',
'ep:goblet-square-full',
'ep:gold-medal',
'ep:goods',
'ep:goods-filled',
'ep:grape',
'ep:grid',
'ep:guide',
'ep:handbag',
'ep:headset',
'ep:help',
'ep:help-filled',
'ep:hide',
'ep:histogram',
'ep:home-filled',
'ep:hot-water',
'ep:house',
'ep:ice-cream',
'ep:ice-cream-round',
'ep:ice-cream-square',
'ep:ice-drink',
'ep:ice-tea',
'ep:info-filled',
'ep:iphone',
'ep:key',
'ep:knife-fork',
'ep:lightning',
'ep:link',
'ep:list',
'ep:loading',
'ep:location',
'ep:location-filled',
'ep:location-information',
'ep:lock',
'ep:lollipop',
'ep:magic-stick',
'ep:magnet',
'ep:male',
'ep:management',
'ep:map-location',
'ep:medal',
'ep:memo',
'ep:menu',
'ep:message',
'ep:message-box',
'ep:mic',
'ep:microphone',
'ep:milk-tea',
'ep:minus',
'ep:money',
'ep:monitor',
'ep:moon',
'ep:moon-night',
'ep:more',
'ep:more-filled',
'ep:mostly-cloudy',
'ep:mouse',
'ep:mug',
'ep:mute',
'ep:mute-notification',
'ep:no-smoking',
'ep:notebook',
'ep:notification',
'ep:odometer',
'ep:office-building',
'ep:open',
'ep:operation',
'ep:opportunity',
'ep:orange',
'ep:paperclip',
'ep:partly-cloudy',
'ep:pear',
'ep:phone',
'ep:phone-filled',
'ep:picture',
'ep:picture-filled',
'ep:picture-rounded',
'ep:pie-chart',
'ep:place',
'ep:platform',
'ep:plus',
'ep:pointer',
'ep:position',
'ep:postcard',
'ep:pouring',
'ep:present',
'ep:price-tag',
'ep:printer',
'ep:promotion',
'ep:quartz-watch',
'ep:question-filled',
'ep:rank',
'ep:reading',
'ep:reading-lamp',
'ep:refresh',
'ep:refresh-left',
'ep:refresh-right',
'ep:refrigerator',
'ep:remove',
'ep:remove-filled',
'ep:right',
'ep:scale-to-original',
'ep:school',
'ep:scissor',
'ep:search',
'ep:select',
'ep:sell',
'ep:semi-select',
'ep:service',
'ep:set-up',
'ep:setting',
'ep:share',
'ep:ship',
'ep:shop',
'ep:shopping-bag',
'ep:shopping-cart',
'ep:shopping-cart-full',
'ep:shopping-trolley',
'ep:smoking',
'ep:soccer',
'ep:sold-out',
'ep:sort',
'ep:sort-down',
'ep:sort-up',
'ep:stamp',
'ep:star',
'ep:star-filled',
'ep:stopwatch',
'ep:success-filled',
'ep:sugar',
'ep:suitcase',
'ep:suitcase-line',
'ep:sunny',
'ep:sunrise',
'ep:sunset',
'ep:switch',
'ep:switch-button',
'ep:switch-filled',
'ep:takeaway-box',
'ep:ticket',
'ep:tickets',
'ep:timer',
'ep:toilet-paper',
'ep:tools',
'ep:top',
'ep:top-left',
'ep:top-right',
'ep:trend-charts',
'ep:trophy',
'ep:trophy-base',
'ep:turn-off',
'ep:umbrella',
'ep:unlock',
'ep:upload',
'ep:upload-filled',
'ep:user',
'ep:user-filled',
'ep:van',
'ep:video-camera',
'ep:video-camera-filled',
'ep:video-pause',
'ep:video-play',
'ep:view',
'ep:wallet',
'ep:wallet-filled',
'ep:warn-triangle-filled',
'ep:warning',
'ep:warning-filled',
'ep:watch',
'ep:watermelon',
'ep:wind-power',
'ep:zoom-in',
'ep:zoom-out'
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
import ImageCropping from './src/ImageCropping.vue'
export { ImageCropping }

View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import { useDesign } from '@/hooks/web/useDesign'
import { nextTick, unref, ref, watch, onBeforeUnmount, onMounted, computed } from 'vue'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.min.css'
import { ElDivider, ElUpload, UploadFile, ElMessage, ElTooltip } from 'element-plus'
import { useDebounceFn } from '@vueuse/core'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('image-cropping')
const props = defineProps({
imageUrl: {
type: String,
default: '',
required: true
},
cropBoxWidth: {
type: Number,
default: 200
},
cropBoxHeight: {
type: Number,
default: 200
},
boxWidth: {
type: [Number, String],
default: 425
},
boxHeight: {
type: [Number, String],
default: 320
},
showResult: {
type: Boolean,
default: true
},
showActions: {
type: Boolean,
default: true
}
})
const getBase64 = useDebounceFn(() => {
imgBase64.value = unref(cropperRef)?.getCroppedCanvas()?.toDataURL() ?? ''
}, 80)
const resetCropBox = () => {
const containerData = unref(cropperRef)?.getContainerData()
unref(cropperRef)?.setCropBoxData({
width: props.cropBoxWidth,
height: props.cropBoxHeight,
left: (containerData?.width || 0) / 2 - 100,
top: (containerData?.height || 0) / 2 - 100
})
imgBase64.value = unref(cropperRef)?.getCroppedCanvas()?.toDataURL() ?? ''
}
const getBoxStyle = computed(() => {
return {
width: `${props.boxWidth}px`,
height: `${props.boxHeight}px`
}
})
const getCropBoxStyle = computed(() => {
return {
width: `${props.cropBoxWidth}px`,
height: `${props.cropBoxHeight}px`
}
})
//
const getScaleSize = (scale: number) => {
return {
width: props.cropBoxWidth * scale + 'px',
height: props.cropBoxHeight * scale + 'px'
}
}
const imgBase64 = ref('')
const imgRef = ref<HTMLImageElement>()
const cropperRef = ref<Cropper>()
const intiCropper = () => {
if (!unref(imgRef)) return
const imgEl = unref(imgRef)!
cropperRef.value = new Cropper(imgEl, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
// cropBoxResizable: false,
// cropBoxMovable: false,
toggleDragModeOnDblclick: false,
checkCrossOrigin: false,
ready() {
resetCropBox()
},
cropmove() {
getBase64()
},
zoom() {
getBase64()
},
crop() {
getBase64()
}
})
}
const uploadChange = (uploadFile: UploadFile) => {
//
if (uploadFile?.raw?.type.indexOf('image') === -1) {
ElMessage.error('请上传图片格式的文件')
return
}
if (!uploadFile.raw) return
// 访
const url = URL.createObjectURL(uploadFile.raw)
unref(cropperRef)?.replace(url)
}
const reset = () => {
unref(cropperRef)?.reset()
}
const rotate = (deg: number) => {
unref(cropperRef)?.rotate(deg)
}
const scaleX = ref(1)
const scaleY = ref(1)
const scale = (type: 'scaleX' | 'scaleY') => {
if (type === 'scaleX') {
scaleX.value = scaleX.value === 1 ? -1 : 1
unref(cropperRef)?.[type](unref(scaleX))
} else {
scaleY.value = scaleY.value === 1 ? -1 : 1
unref(cropperRef)?.[type](unref(scaleY))
}
}
const zoom = (num: number) => {
unref(cropperRef)?.zoom(num)
}
onMounted(() => {
intiCropper()
})
watch(
() => props.imageUrl,
async (url) => {
if (url) {
unref(cropperRef)?.replace(url)
await nextTick()
resetCropBox()
}
}
)
onBeforeUnmount(() => {
unref(cropperRef)?.destroy()
})
defineExpose({
cropperExpose: cropperRef
})
</script>
<template>
<div
:class="{
[prefixCls]: true,
'flex items-center': showResult
}"
>
<div>
<div :style="getBoxStyle" class="flex justify-center items-center">
<img
v-show="imageUrl"
ref="imgRef"
:src="imageUrl"
class="block max-w-full"
crossorigin="anonymous"
alt=""
srcset=""
/>
</div>
<div v-if="showActions" class="mt-10px flex items-center">
<div class="flex items-center">
<ElTooltip content="选择文件" placement="bottom">
<ElUpload
action="''"
accept="image/*"
:auto-upload="false"
:show-file-list="false"
:on-change="uploadChange"
>
<BaseButton size="small" type="primary" class="mt-2px"
><Icon icon="ep:upload-filled"
/></BaseButton>
</ElUpload>
</ElTooltip>
</div>
<div class="flex items-center justify-end flex-1">
<ElTooltip content="重置" placement="bottom">
<BaseButton size="small" type="primary" @click="reset"
><Icon icon="ep:refresh"
/></BaseButton>
</ElTooltip>
<ElTooltip content="逆时针旋转" placement="bottom">
<BaseButton size="small" type="primary" @click="rotate(-45)"
><Icon icon="ant-design:rotate-left-outlined"
/></BaseButton>
</ElTooltip>
<ElTooltip content="顺时针旋转" placement="bottom">
<BaseButton size="small" type="primary" @click="rotate(45)"
><Icon icon="ant-design:rotate-right-outlined"
/></BaseButton>
</ElTooltip>
<ElTooltip content="水平翻转" placement="bottom">
<BaseButton size="small" type="primary" @click="scale('scaleX')"
><Icon icon="vaadin:arrows-long-h"
/></BaseButton>
</ElTooltip>
<ElTooltip content="垂直翻转" placement="bottom">
<BaseButton size="small" type="primary" @click="scale('scaleY')"
><Icon icon="vaadin:arrows-long-v"
/></BaseButton>
</ElTooltip>
<ElTooltip content="放大" placement="bottom">
<BaseButton size="small" type="primary" @click="zoom(0.1)"
><Icon icon="ant-design:zoom-in-outlined"
/></BaseButton>
</ElTooltip>
<ElTooltip content="缩小" placement="bottom">
<BaseButton size="small" type="primary" @click="zoom(-0.1)"
><Icon icon="ant-design:zoom-out-outlined"
/></BaseButton>
</ElTooltip>
</div>
</div>
</div>
<div v-if="imgBase64 && showResult" class="ml-20px">
<div class="flex justify-center items-center">
<img :src="imgBase64" class="rounded-[50%]" :style="getCropBoxStyle" />
</div>
<ElDivider />
<div class="flex justify-center items-center">
<img :src="imgBase64" class="rounded-[50%]" :style="getScaleSize(0.2)" />
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.25)" />
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.3)" />
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.35)" />
</div>
</div>
</div>
</template>

View File

@ -86,7 +86,7 @@ const getPasswordStrength = computed(() => {
background-color: transparent;
border-color: var(--el-color-white);
border-style: solid;
border-width: 0 5px 0 5px;
border-width: 0 5px;
content: '';
}

View File

@ -30,13 +30,7 @@ watch(
show.value = true
return
}
if (!collapse) {
setTimeout(() => {
show.value = !collapse
}, 400)
} else {
show.value = !collapse
}
show.value = !collapse
}
)

View File

@ -93,7 +93,7 @@ export default defineComponent({
>
{{
default: () => {
const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
const { renderMenuItem } = useRenderMenuItem()
return renderMenuItem(unref(routers))
}
}}
@ -123,30 +123,10 @@ export default defineComponent({
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-menu';
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls} {
position: relative;
transition: width var(--transition-time-02);
// &:after {
// position: absolute;
// top: 0;
// right: 0;
// height: 100%;
// width: 1px;
// background-color: var(--el-border-color);
// content: '';
// }
:deep(.@{elNamespace}-menu) {
width: 100% !important;
border-right: none;
@ -168,7 +148,6 @@ export default defineComponent({
}
//
.@{elNamespace}-sub-menu.is-active,
.@{elNamespace}-menu-item.is-active {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important;
@ -180,10 +159,6 @@ export default defineComponent({
.@{elNamespace}-menu-item.is-active {
position: relative;
// &:after {
// .is-active--after;
// }
}
//
@ -203,16 +178,11 @@ export default defineComponent({
& > .is-active > .@{elNamespace}-sub-menu__title {
position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important;
// &:after {
// .is-active--after;
// }
}
}
//
:deep(.horizontal-collapse-transition) {
// transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
.@{prefix-cls}__title {
display: none;
}
@ -235,7 +205,7 @@ export default defineComponent({
.@{elNamespace}-menu-item.is-active {
position: relative;
&:after {
&::after {
display: none !important;
}
}
@ -254,16 +224,6 @@ export default defineComponent({
<style lang="less">
@prefix-cls: ~'@{namespace}-menu-popper';
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls}--vertical,
.@{prefix-cls}--horizontal {
//
@ -290,10 +250,6 @@ export default defineComponent({
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
// &:after {
// .is-active--after;
// }
}
}
</style>

View File

@ -2,57 +2,49 @@ import { ElSubMenu, ElMenuItem } from 'element-plus'
import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle'
import { useDesign } from '@/hooks/web/useDesign'
import { pathResolve } from '@/utils/routerHelper'
const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = (
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [],
menuMode: 'vertical' | 'horizontal'
) => {
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers.map((v) => {
const meta = v.meta ?? {}
if (!meta.hidden) {
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
{
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers
.filter((v) => !v.meta?.hidden)
.map((v) => {
const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
const { getPrefixCls } = useDesign()
if (
oneShowingChild &&
(!(onlyOneChild?.children?.length !== 0) || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
return (
<ElSubMenu index={fullPath}>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
})
}
const preFixCls = getPrefixCls('menu-popper')
return (
<ElSubMenu
index={fullPath}
popperClass={
menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
}
>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
}
})
return {
renderMenuItem
}
}
return {
renderMenuItem
}
}

View File

@ -10,10 +10,14 @@ export const useRenderMenuTitle = () => {
return icon ? (
<>
<Icon icon={meta.icon}></Icon>
<span class="v-menu__title">{t(title as string)}</span>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</>
) : (
<span class="v-menu__title">{t(title as string)}</span>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
)
}

View File

@ -242,7 +242,7 @@ const disabledClick = () => {
.@{prefix-cls} {
&--disabled {
background: rgba(255, 255, 255, 0.95);
background: rgb(255 255 255 / 95%);
& > div {
transform: translate(-50%, -50%);

View File

@ -88,6 +88,9 @@ const newSchema = computed(() => {
/>
</div>
)
},
label: () => {
return <span>&nbsp;</span>
}
}
}
@ -117,11 +120,14 @@ const setProps = (props: SearchProps = {}) => {
outsideProps.value = props
}
const schemaRef = ref<FormSchema[]>([])
// formModel
watch(
() => unref(newSchema),
async (schema = []) => {
formModel.value = initModel(schema, unref(formModel))
schemaRef.value = schema
},
{
immediate: true,
@ -241,7 +247,7 @@ const onFormValidate = (prop: FormItemProp, isValid: boolean, message: string) =
hide-required-asterisk
:inline="getProps.inline"
:is-col="getProps.isCol"
:schema="newSchema"
:schema="schemaRef"
@register="formRegister"
@validate="onFormValidate"
/>

View File

@ -1,5 +1,4 @@
<script setup lang="ts">
import { ElButton } from 'element-plus'
import { useIcon } from '@/hooks/web/useIcon'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from '@/hooks/web/useI18n'
@ -31,7 +30,7 @@ const onExpand = () => {
</script>
<template>
<ElButton
<BaseButton
v-if="showSearch"
type="primary"
:loading="searchLoading"
@ -39,21 +38,21 @@ const onExpand = () => {
@click="onSearch"
>
{{ t('common.query') }}
</ElButton>
<ElButton
</BaseButton>
<BaseButton
v-if="showReset"
:loading="resetLoading"
:icon="useIcon({ icon: 'ep:refresh-right' })"
@click="onReset"
>
{{ t('common.reset') }}
</ElButton>
<ElButton
</BaseButton>
<BaseButton
v-if="showExpand"
:icon="useIcon({ icon: visible ? 'ep:arrow-down' : 'ep:arrow-up' })"
:icon="useIcon({ icon: visible ? 'ep:arrow-up' : 'ep:arrow-down' })"
text
@click="onExpand"
>
{{ t(visible ? 'common.shrink' : 'common.expand') }}
</ElButton>
</BaseButton>
</template>

View File

@ -1,12 +1,11 @@
<script setup lang="ts">
import { ElDrawer, ElDivider, ElButton, ElMessage } from 'element-plus'
import { ref, unref, computed, watch } from 'vue'
import { ElDrawer, ElDivider, ElMessage } from 'element-plus'
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
import { useCssVar } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { trim, setCssVar } from '@/utils'
import { trim, setCssVar, getCssVar } from '@/utils'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
@ -14,7 +13,7 @@ import { useStorage } from '@/hooks/web/useStorage'
import { useClipboard } from '@vueuse/core'
import { useDesign } from '@/hooks/web/useDesign'
const { removeStorage } = useStorage()
const { clear: storageClear } = useStorage('localStorage')
const { getPrefixCls } = useDesign()
@ -24,8 +23,6 @@ const appStore = useAppStore()
const { t } = useI18n()
const layout = computed(() => appStore.getLayout)
const drawer = ref(false)
//
@ -42,70 +39,27 @@ const setSystemTheme = (color: string) => {
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
const setHeaderTheme = (color: string) => {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (unref(layout) === 'top') {
setMenuTheme(color)
}
appStore.setHeaderTheme(color)
}
//
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
const setMenuTheme = (color: string) => {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
//
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
//
leftMenuBgColor: color,
//
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
//
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
//
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo
logoBorderColor: isDarkColor ? color : '#eee'
}
appStore.setTheme(theme)
appStore.setCssVarTheme()
appStore.setMenuTheme(color)
}
// layout
watch(
() => layout.value,
(n) => {
if (n === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
} else {
setMenuTheme(unref(menuTheme))
}
}
)
// watch(
// () => layout.value,
// (n) => {
// if (n === 'top' && !appStore.getIsDark) {
// headerTheme.value = '#fff'
// setHeaderTheme('#fff')
// } else {
// setMenuTheme(unref(menuTheme))
// }
// }
// )
//
const copyConfig = async () => {
@ -174,7 +128,8 @@ const copyConfig = async () => {
//
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
}
`
`,
legacy: true
})
if (!isSupported) {
ElMessage.error(t('setting.copyFailed'))
@ -188,11 +143,15 @@ const copyConfig = async () => {
//
const clear = () => {
removeStorage('layout')
removeStorage('theme')
removeStorage('isDark')
storageClear()
window.location.reload()
}
const themeChange = () => {
const color = getCssVar('--el-bg-color')
setMenuTheme(color)
setHeaderTheme(color)
}
</script>
<template>
@ -212,7 +171,7 @@ const clear = () => {
<div class="text-center">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<ThemeSwitch @change="themeChange" />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>
@ -253,23 +212,21 @@ const clear = () => {
/>
<!-- 菜单主题 -->
<template v-if="layout !== 'top'">
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</template>
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</div>
<!-- 界面显示 -->
@ -278,12 +235,14 @@ const clear = () => {
<ElDivider />
<div>
<ElButton type="primary" class="w-full" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
<BaseButton type="primary" class="w-full" @click="copyConfig">{{
t('setting.copy')
}}</BaseButton>
</div>
<div class="mt-5px">
<ElButton type="danger" class="w-full" @click="clear">
<BaseButton type="danger" class="w-full" @click="clear">
{{ t('setting.clearAndReset') }}
</ElButton>
</BaseButton>
</div>
</ElDrawer>
</template>

View File

@ -67,7 +67,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -79,7 +79,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;
@ -95,7 +95,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -107,7 +107,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;
@ -123,7 +123,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -140,7 +140,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -152,7 +152,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;

View File

@ -1,12 +1,11 @@
<script setup lang="ts">
import { ElDrawer, ElDivider, ElButton, ElMessage } from 'element-plus'
import { ref, unref, computed, watch } from 'vue'
import { ElDrawer, ElDivider, ElMessage } from 'element-plus'
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
import { useCssVar } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { trim, setCssVar } from '@/utils'
import { trim, setCssVar, getCssVar } from '@/utils'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
@ -15,7 +14,7 @@ import { useClipboard } from '@vueuse/core'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
const { removeStorage } = useStorage()
const { clear: storageClear } = useStorage('localStorage')
const { getPrefixCls } = useDesign()
@ -29,8 +28,6 @@ const appStore = useAppStore()
const { t } = useI18n()
const layout = computed(() => appStore.getLayout)
const drawer = ref(false)
//
@ -47,70 +44,27 @@ const setSystemTheme = (color: string) => {
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
const setHeaderTheme = (color: string) => {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (unref(layout) === 'top') {
setMenuTheme(color)
}
appStore.setHeaderTheme(color)
}
//
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
const setMenuTheme = (color: string) => {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
//
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
//
leftMenuBgColor: color,
//
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
//
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
//
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo
logoBorderColor: isDarkColor ? color : '#eee'
}
appStore.setTheme(theme)
appStore.setCssVarTheme()
appStore.setMenuTheme(color)
}
// layout
watch(
() => layout.value,
(n) => {
if (n === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
} else {
setMenuTheme(unref(menuTheme))
}
}
)
// watch(
// () => layout.value,
// (n) => {
// if (n === 'top' && !appStore.getIsDark) {
// headerTheme.value = '#fff'
// setHeaderTheme('#fff')
// } else {
// setMenuTheme(unref(menuTheme))
// }
// }
// )
//
const copyConfig = async () => {
@ -179,7 +133,8 @@ const copyConfig = async () => {
//
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
}
`
`,
legacy: true
})
if (!isSupported) {
ElMessage.error(t('setting.copyFailed'))
@ -193,11 +148,15 @@ const copyConfig = async () => {
//
const clear = () => {
removeStorage('layout')
removeStorage('theme')
removeStorage('isDark')
storageClear()
window.location.reload()
}
const themeChange = () => {
const color = getCssVar('--el-bg-color')
setMenuTheme(color)
setHeaderTheme(color)
}
</script>
<template>
@ -219,7 +178,7 @@ const clear = () => {
<div class="text-center">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<ThemeSwitch @change="themeChange" />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>
@ -260,23 +219,21 @@ const clear = () => {
/>
<!-- 菜单主题 -->
<template v-if="layout !== 'top'">
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</template>
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</div>
<!-- 界面显示 -->
@ -285,14 +242,14 @@ const clear = () => {
<ElDivider />
<div>
<ElButton type="primary" class="w-full" @click="copyConfig">{{
<BaseButton type="primary" class="w-full" @click="copyConfig">{{
t('setting.copy')
}}</ElButton>
}}</BaseButton>
</div>
<div class="mt-5px">
<ElButton type="danger" class="w-full" @click="clear">
<BaseButton type="danger" class="w-full" @click="clear">
{{ t('setting.clearAndReset') }}
</ElButton>
</BaseButton>
</div>
</ElDrawer>
</div>

View File

@ -67,7 +67,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -79,7 +79,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;
@ -95,7 +95,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -107,7 +107,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;
@ -123,7 +123,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -140,7 +140,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb;
border-radius: 4px;
&:before {
&::before {
position: absolute;
top: 0;
left: 0;
@ -152,7 +152,7 @@ const layout = computed(() => appStore.getLayout)
content: '';
}
&:after {
&::after {
position: absolute;
top: 0;
left: 0;

View File

@ -88,6 +88,9 @@ export default defineComponent({
} else {
showTitle.value = !collapse
}
},
{
immediate: true
}
)
@ -202,11 +205,12 @@ export default defineComponent({
</div>
<Menu
class={[
'!absolute top-0 z-4000',
'!absolute top-0 z-3000',
{
'!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu),
'!w-[var(--left-menu-max-width)] border-r-1 border-r-solid border-[var(--el-border-color)]':
unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu)
}
]}

View File

@ -5,7 +5,9 @@ import {
ElPagination,
ComponentSize,
ElTooltipProps,
ElImage
ElImage,
ElEmpty,
ElCard
} from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
@ -15,9 +17,10 @@ import { set, get } from 'lodash-es'
import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue'
// import Sortable from 'sortablejs'
// import { Icon } from '@/components/Icon'
import { useAppStore } from '@/store/modules/app'
import { createVideoViewer } from '@/components/VideoPlayer'
import { Icon } from '@/components/Icon'
import { BaseButton } from '@/components/Button'
const appStore = useAppStore()
@ -60,8 +63,13 @@ export default defineComponent({
type: Array as PropType<Recordable[]>,
default: () => []
},
//
preview: {
//
imagePreview: {
type: Array as PropType<string[]>,
default: () => []
},
//
videoPreview: {
type: Array as PropType<string[]>,
default: () => []
},
@ -72,7 +80,7 @@ export default defineComponent({
border: propTypes.bool.def(true),
size: {
type: String as PropType<ComponentSize>,
validator: (v: ComponentSize) => ['medium', 'small', 'mini'].includes(v)
validator: (v: ComponentSize) => ['default', 'small', 'large'].includes(v)
},
fit: propTypes.bool.def(true),
showHeader: propTypes.bool.def(true),
@ -191,9 +199,27 @@ export default defineComponent({
default: 'fixed'
},
scrollbarAlwaysOn: propTypes.bool.def(false),
flexible: propTypes.bool.def(false)
flexible: propTypes.bool.def(false),
//
customContent: propTypes.bool.def(false),
cardBodyStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({})
},
cardBodyClass: {
type: String as PropType<string>,
default: ''
},
cardWrapStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({})
},
cardWrapClass: {
type: String as PropType<string>,
default: ''
}
},
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh', 'sortable-change'],
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh'],
setup(props, { attrs, emit, slots, expose }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>()
@ -218,33 +244,6 @@ export default defineComponent({
return propsObj
})
// const sortableEl = ref()
//
// const initDropTable = () => {
// const el = unref(elTableRef)?.$el.querySelector('.el-table__body tbody')
// if (!el) return
// if (unref(sortableEl)) unref(sortableEl).destroy()
// sortableEl.value = Sortable.create(el, {
// handle: '.table-move',
// animation: 180,
// onEnd(e: any) {
// emit('sortable-change', e)
// }
// })
// }
// watch(
// () => getProps.value.sortable,
// async (v) => {
// await nextTick()
// v && initDropTable()
// },
// {
// immediate: true
// }
// )
const setProps = (props: TableProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = { ...props } as any
@ -265,7 +264,7 @@ export default defineComponent({
const addColumn = (column: TableColumn, index?: number) => {
const { columns } = unref(getProps)
if (index) {
if (index !== void 0) {
columns.splice(index, 0, column)
} else {
columns.push(column)
@ -350,11 +349,13 @@ export default defineComponent({
const bindValue: Recordable = { ...attrs, ...unref(getProps) }
delete bindValue.columns
delete bindValue.data
delete bindValue.align
return bindValue
})
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
const { align, headerAlign, showOverflowTooltip, imagePreview, videoPreview } =
unref(getProps)
return columnsChildren.map((v) => {
if (v.show === false) return null
const props = { ...v } as any
@ -365,20 +366,20 @@ export default defineComponent({
const slots = {
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
let isPreview = false
isPreview =
imagePreview.some((item) => (item as string) === v.field) ||
videoPreview.some((item) => (item as string) === v.field)
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(...args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl
? renderPreview(get(data.row, v.field))
: get(data.row, v.field)
? props.slots.default(...args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isPreview
? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field)
}
}
if (props?.slots?.header) {
@ -399,17 +400,32 @@ export default defineComponent({
})
}
const renderPreview = (url: string) => {
const renderPreview = (url: string, field: string) => {
const { imagePreview, videoPreview } = unref(getProps)
return (
<div class="flex items-center">
<ElImage
src={url}
fit="cover"
class="w-[100%] h-100px"
lazy
preview-src-list={[url]}
preview-teleported
/>
{imagePreview.includes(field) ? (
<ElImage
src={url}
fit="cover"
class="w-[100%]"
lazy
preview-src-list={[url]}
preview-teleported
/>
) : videoPreview.includes(field) ? (
<BaseButton
type="primary"
icon={<Icon icon="ep:video-play" />}
onClick={() => {
createVideoViewer({
url
})
}}
>
预览
</BaseButton>
) : null}
</div>
)
}
@ -424,7 +440,8 @@ export default defineComponent({
headerAlign,
showOverflowTooltip,
reserveSelection,
preview
imagePreview,
videoPreview
} = unref(getProps)
return (columnsChildren || columns).map((v) => {
@ -450,6 +467,7 @@ export default defineComponent({
reserveSelection={reserveSelection}
align="center"
headerAlign="center"
selectable={v.selectable}
width="50px"
fixed="left"
></ElTableColumn>
@ -464,20 +482,20 @@ export default defineComponent({
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
let isPreview = false
isPreview =
imagePreview.some((item) => (item as string) === v.field) ||
videoPreview.some((item) => (item as string) === v.field)
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(...args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl
? renderPreview(get(data.row, v.field))
: get(data.row, v.field)
? props.slots.default(...args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isPreview
? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field)
}
}
if (props?.slots?.header) {
@ -509,52 +527,79 @@ export default defineComponent({
}
const toolbar = getSlot(slots, 'toolbar')
// const { sortable } = unref(getProps)
// const sortableEl = sortable ? (
// <ElTableColumn
// className="table-move cursor-move"
// type="sortable"
// prop="sortable"
// width="60px"
// align="center"
// >
// <Icon icon="ant-design:drag-outlined" />
// </ElTableColumn>
// ) : null
return (
<div v-loading={unref(getProps).loading}>
<div class="flex justify-between mb-1">
<div>{toolbar}</div>
<div class="pt-2">
{unref(getProps).showAction ? (
<TableActions
activeUID={unref(getProps).activeUID}
columns={unref(getProps).columns}
el-table-ref={elTableRef}
onChangSize={changSize}
onRefresh={refresh}
/>
) : null}
{unref(getProps).customContent ? (
<div class="flex flex-wrap">
{unref(getProps)?.data?.length ? (
unref(getProps)?.data.map((item) => {
const cardSlots = {
default: () => {
return getSlot(slots, 'content', item)
}
}
if (getSlot(slots, 'content-header')) {
cardSlots['header'] = () => {
return getSlot(slots, 'content-header', item)
}
}
if (getSlot(slots, 'content-footer')) {
cardSlots['footer'] = () => {
return getSlot(slots, 'content-footer', item)
}
}
return (
<ElCard
shadow="hover"
class={unref(getProps).cardWrapClass}
style={unref(getProps).cardWrapStyle}
bodyClass={unref(getProps).cardBodyClass}
bodyStyle={unref(getProps).cardBodyStyle}
>
{cardSlots}
</ElCard>
)
})
) : (
<div class="flex flex-1 justify-center">
<ElEmpty description="暂无数据" />
</div>
)}
</div>
</div>
) : (
<>
<div class="flex justify-between mb-1">
<div>{toolbar}</div>
<div class="pt-2">
{unref(getProps).showAction ? (
<TableActions
activeUID={unref(getProps).activeUID}
columns={unref(getProps).columns}
el-table-ref={elTableRef}
onChangSize={changSize}
onRefresh={refresh}
/>
) : null}
</div>
</div>
<ElTable
ref={elTableRef}
data={unref(getProps).data}
{...unref(getBindValue)}
header-cell-style={
appStore.getIsDark
? { color: '#CFD3DC', 'background-color': '#000' }
: { color: '#000', 'background-color': '#f5f7fa' }
}
>
{{
default: () => renderTableColumn(),
...tableSlots
}}
</ElTable>
</>
)}
<ElTable
ref={elTableRef}
data={unref(getProps).data}
{...unref(getBindValue)}
header-cell-style={
appStore.getIsDark
? { color: '#CFD3DC', 'background-color': '#000' }
: { color: '#000', 'background-color': '#f5f7fa' }
}
>
{{
default: () => renderTableColumn(),
...tableSlots
}}
</ElTable>
{unref(getProps).pagination ? (
<ElPagination
v-model:pageSize={pageSizeRef.value}

View File

@ -9,7 +9,6 @@ import {
ElPopover,
ElCheckbox,
ElScrollbar,
ElButton,
ElTable,
ElDivider
} from 'element-plus'
@ -23,14 +22,10 @@ import { useStorage } from '@/hooks/web/useStorage'
import cloneDeep from 'lodash/cloneDeep'
import { propTypes } from '@/utils/propTypes'
import { moveElementToIndex } from '@/utils/index'
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
import { BaseButton } from '@/components/Button'
const { setStorage, getStorage, removeStorage } = useStorage()
const { t } = useI18n()
export default defineComponent({
name: 'TableActions',
props: {
@ -47,6 +42,10 @@ export default defineComponent({
},
emits: ['refresh', 'changSize'],
setup(props, { emit }) {
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const { t } = useI18n()
const refresh = () => {
emit('refresh')
}
@ -263,9 +262,9 @@ export default defineComponent({
{t('common.SerialNumberColumn')}
</ElCheckbox>
</div>
<ElButton type="primary" link onClick={resetTableColumns}>
<BaseButton type="primary" link onClick={resetTableColumns}>
{t('common.reset')}
</ElButton>
</BaseButton>
</div>
<ElScrollbar max-height="400px">
<VueDraggable

View File

@ -210,13 +210,14 @@ const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
//
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
//
//
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) {
for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose()
setSelectTag(tagItem)
}
}
}
@ -582,4 +583,3 @@ watch(
}
}
</style>
@/hooks/web/useTagsView

View File

@ -9,6 +9,8 @@ const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('theme-switch')
const emit = defineEmits(['change'])
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
@ -23,6 +25,7 @@ const blackColor = 'var(--el-color-black)'
const themeChange = (val: boolean) => {
appStore.setIsDark(val)
emit('change', val)
}
</script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAuthStore } from '@/store/modules/auth'
import { useDesign } from '@/hooks/web/useDesign'
import LockDialog from './components/LockDialog.vue'
import { ref, computed } from 'vue'
@ -14,7 +14,7 @@ const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const { getPrefixCls } = useDesign()
@ -73,13 +73,13 @@ const user = computed(() => authStore.getUser)
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>
<ElButton @click="toHome" link>个人主页</ElButton>
<BaseButton @click="toHome" link>个人主页</BaseButton>
</ElDropdownItem>
<ElDropdownItem>
<ElButton @click="toGitee" link>Gitee</ElButton>
<BaseButton @click="toGitee" link>Gitee</BaseButton>
</ElDropdownItem>
<ElDropdownItem>
<ElButton @click="toGithub" link>Github</ElButton>
<BaseButton @click="toGithub" link>Github</BaseButton>
</ElDropdownItem>
<ElDropdownItem divided>
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>

View File

@ -7,7 +7,6 @@ import { useForm } from '@/hooks/web/useForm'
import { reactive, computed } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/components/Form'
import { ElButton } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
@ -87,14 +86,14 @@ const handleLock = async () => {
</div>
<Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
<BaseButton type="primary" @click="handleLock">{{ t('lock.lock') }}</BaseButton>
</template>
</Dialog>
</template>
<style lang="less" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
@media (width <= 767px) {
max-width: calc(100vw - 16px);
}
}

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { ElInput, ElButton } from 'element-plus'
import { ElInput } from 'element-plus'
import { useLockStore } from '@/store/modules/lock'
import { useI18n } from '@/hooks/web/useI18n'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@/components/Icon'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAuthStore } from '@/store/modules/auth'
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const password = ref('')
const loading = ref(false)
@ -92,7 +92,7 @@ function handleShowForm(show = false) {
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
<BaseButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
@ -101,8 +101,8 @@ function handleShowForm(show = false) {
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
</BaseButton>
<BaseButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
@ -111,8 +111,8 @@ function handleShowForm(show = false) {
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
</BaseButton>
<BaseButton
type="primary"
class="mt-2"
size="small"
@ -121,7 +121,7 @@ function handleShowForm(show = false) {
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</BaseButton>
</div>
</div>
</div>
@ -190,6 +190,7 @@ function handleShowForm(show = false) {
font-size: 90px;
}
}
@media screen and (min-width: @screen-lg) {
span:not(.meridiem) {
font-size: 220px;
@ -201,6 +202,7 @@ function handleShowForm(show = false) {
font-size: 260px;
}
}
@media screen and (min-width: @screen-2xl) {
span:not(.meridiem) {
font-size: 320px;

View File

@ -0,0 +1,27 @@
import { VNode, createVNode, render } from 'vue'
import VideoPlayer from './src/VideoPlayer.vue'
import { isClient } from '@/utils/is'
import { VideoPlayerViewer } from '@/components/VideoPlayerViewer'
import { toAnyString } from '@/utils'
export { VideoPlayer }
let instance: Nullable<VNode> = null
export function createVideoViewer(options: { url: string; poster?: string; show?: boolean }) {
if (!isClient) return
const { url, poster } = options
const propsData: Partial<{ url: string; poster?: string; show?: boolean; id?: string }> = {}
const container = document.createElement('div')
const id = toAnyString()
container.id = id
propsData.url = url
propsData.poster = poster
propsData.show = true
propsData.id = id
document.body.appendChild(container)
instance = createVNode(VideoPlayerViewer, propsData)
render(instance, container)
}

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import Player from 'xgplayer'
import { ref, unref, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
import 'xgplayer/dist/index.min.css'
const props = defineProps({
url: {
type: String,
default: '',
required: true
},
poster: {
type: String,
default: ''
}
})
const playerRef = ref<Player>()
const videoEl = ref<HTMLDivElement>()
const intiPlayer = () => {
if (!unref(videoEl)) return
new Player({
autoplay: false,
...props,
el: unref(videoEl)
})
}
onMounted(() => {
intiPlayer()
})
watch(
() => props,
async (newProps) => {
await nextTick()
if (newProps) {
unref(playerRef)?.setConfig(newProps)
}
},
{
deep: true
}
)
onBeforeUnmount(() => {
unref(playerRef)?.destroy()
})
defineExpose({
playerExpose: () => unref(playerRef)
})
</script>
<template>
<div ref="videoEl"></div>
</template>

View File

@ -0,0 +1,3 @@
import VideoPlayerViewer from './src/VideoPlayerViewer.vue'
export { VideoPlayerViewer }

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import { VideoPlayer } from '@/components/VideoPlayer'
import { ElOverlay } from 'element-plus'
import { ref, nextTick } from 'vue'
import { Icon } from '@/components/Icon'
const props = defineProps({
show: {
type: Boolean,
default: false
},
url: {
type: String,
default: '',
required: true
},
poster: {
type: String,
default: ''
},
id: {
type: String,
default: ''
}
})
const visible = ref(props.show)
const close = async () => {
visible.value = false
await nextTick()
const wrap = document.getElementById(props.id)
if (!wrap) return
document.body.removeChild(wrap)
}
</script>
<template>
<ElOverlay v-show="visible" @click="close">
<div class="w-full h-full flex justify-center items-center relative" @click="close">
<div
class="w-44px h-44px color-[#fff] bg-[var(--el-text-color-regular)] rounded-full border-[#fff] flex justify-center items-center cursor-pointer absolute top-40px right-40px"
@click="close"
>
<Icon icon="ep:close" :size="24" />
</div>
<VideoPlayer :url="url" :poster="poster" />
</div>
</ElOverlay>
</template>

View File

@ -0,0 +1,3 @@
import Waterfall from './src/Waterfall.vue'
export { Waterfall }

View File

@ -0,0 +1,234 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ref, nextTick, unref, onMounted, watch } from 'vue'
import { useEventListener, useIntersectionObserver } from '@vueuse/core'
import { debounce } from 'lodash-es'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('waterfall')
const emit = defineEmits(['loadMore'])
const prop = defineProps({
data: propTypes.arrayOf(propTypes.any),
reset: propTypes.bool.def(true),
width: propTypes.number.def(200),
gap: propTypes.number.def(20),
props: propTypes.objectOf(propTypes.string).def({
src: 'src',
height: 'height'
}),
cols: propTypes.number.def(undefined),
loadingText: propTypes.string.def('加载中...'),
loading: propTypes.bool.def(false),
end: propTypes.bool.def(false),
endText: propTypes.string.def('没有更多了'),
autoCenter: propTypes.bool.def(true),
layout: propTypes.oneOf(['javascript', 'flex']).def('flex')
})
const wrapEl = ref<HTMLDivElement>()
const heights = ref<number[]>([])
const wrapHeight = ref(0)
const wrapWidth = ref(0)
const loadMore = ref<HTMLDivElement>()
// = /
const innerCols = ref(0)
const filterData = ref<any[]>([])
const filterWaterfall = async () => {
filterData.value = []
const { props, width, gap } = prop
const data = prop.data as any[]
await nextTick()
const container = unref(wrapEl) as HTMLElement
if (!container) return
innerCols.value = prop.cols ?? Math.floor(container.clientWidth / (width + gap))
const length = data.length
for (let i = 0; i < length; i++) {
if (i < unref(innerCols)) {
heights.value[i] = data[i][props.height as string]
filterData.value.push({
...data[i],
top: 0,
left: i * (width + gap)
})
} else {
//
//
let minHeight = heights.value[0]
let index = 0
//
for (let j = 1; j < unref(innerCols); j++) {
if (unref(heights)[j] < minHeight) {
minHeight = unref(heights)[j]
index = j
}
}
//
heights.value[index] += data[i][props.height as string] + gap
filterData.value.push({
...data[i],
top: minHeight + gap,
left: index * (width + gap)
})
}
}
wrapHeight.value = Math.max(...unref(heights))
wrapWidth.value = unref(innerCols) * (width + gap) - gap
}
const flexWaterfall = async () => {
const { width, gap } = prop
const data = prop.data as any[]
await nextTick()
const container = unref(wrapEl) as HTMLElement
if (!container) return
innerCols.value = prop.cols ?? Math.floor(container.clientWidth / (width + gap))
const length = data.length
//
const arr = new Array(unref(innerCols)).fill([])
// dataarr
for (let i = 0; i < length; i++) {
const index = i % unref(innerCols)
arr[index] = [...arr[index], data[i]]
}
filterData.value = arr
}
const initLayout = () => {
const { layout } = prop
if (layout === 'javascript') {
filterWaterfall()
} else if (layout === 'flex') {
flexWaterfall()
}
}
watch(
() => [prop.data, prop.cols],
() => {
initLayout()
},
{
immediate: true
}
)
onMounted(() => {
if (unref(prop.reset)) {
useEventListener(window, 'resize', debounce(initLayout, 300))
}
useIntersectionObserver(
unref(loadMore),
([{ isIntersecting }]) => {
if (isIntersecting && !prop.loading && !prop.end) {
emit('loadMore')
}
},
{
threshold: 0.1
}
)
})
</script>
<template>
<div
:class="[
prefixCls,
'flex',
'items-center',
{
'justify-center': autoCenter
}
]"
ref="wrapEl"
:style="{
height: `${layout === 'javascript' ? wrapHeight + 40 : 'auto'}px`
}"
>
<template v-if="layout === 'javascript'">
<div class="relative" :style="{ width: `${wrapWidth}px`, height: `${wrapHeight + 40}px` }">
<div
v-for="(item, $index) in filterData"
:class="[
`${prefixCls}-item__${$index}`,
{
absolute: layout === 'javascript'
}
]"
:key="`water-${$index}`"
:style="{
width: `${width}px`,
height: `${item[props.height as string]}px`,
top: `${item.top}px`,
left: `${item.left}px`
}"
>
<img :src="item[props.src as string]" class="w-full h-full block" alt="" srcset="" />
</div>
<div
ref="loadMore"
class="h-40px flex justify-center absolute w-full"
:style="{
top: `${wrapHeight + gap}px`
}"
>
{{ end ? endText : loadingText }}
</div>
</div>
</template>
<template v-else-if="layout === 'flex'">
<div
class="relative flex pb-40px"
:style="{
width: cols ? '100%' : 'auto'
}"
>
<div
v-for="(item, $index) in filterData"
:key="`waterWrap-${$index}`"
class="flex-1"
:style="{
marginRight: $index === filterData.length - 1 ? '0' : `${gap}px`
}"
>
<div
v-for="(child, i) in item"
:key="`waterWrap-${$index}-${i}`"
:style="{
marginBottom: `${gap}px`,
width: cols ? '100%' : `${width}px`,
height: cols ? 'auto' : `${child[props.height as string]}px`
}"
>
<img :src="child[props.src as string]" class="w-full h-full block" alt="" srcset="" />
</div>
</div>
<div
ref="loadMore"
class="h-40px flex justify-center absolute w-full items-center"
:style="{
bottom: 0
}"
>
{{ end ? endText : loadingText }}
</div>
</div>
</template>
</div>
</template>

View File

@ -1,8 +1,10 @@
import type { App } from 'vue'
import { Icon } from './Icon'
import { Permission } from './Permission'
import { BaseButton } from './Button'
export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon)
app.component('Permission', Permission)
app.component('BaseButton', BaseButton)
}

View File

@ -1,7 +1,5 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { useStorage } from '@/hooks/web/useStorage'
import { useAppStore } from '@/store/modules/app'
import { useAuthStore } from '@/store/modules/auth'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import qs from 'qs'
import { config } from './config'
import { ElMessage } from 'element-plus'
@ -9,8 +7,6 @@ import request from '@/config/axios'
const { result_code, unauthorized_code, request_timeout } = config
const { getStorage, setStorage } = useStorage()
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: '/api', // api 的 base_url
@ -21,10 +17,10 @@ const service: AxiosInstance = axios.create({
// request拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const appStore = useAppStore()
const token = getStorage(appStore.getToken)
const authStore = useAuthStoreWithOut()
const token = authStore.getToken
if (token !== '') {
;(config.headers as any)['Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
;(config.headers as any)[authStore.getTokenKey ?? 'Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
}
if (
config.method === 'post' &&
@ -87,18 +83,18 @@ service.interceptors.response.use(
if (refresh === '1') {
// 因token快过期刷新token
refreshToken().then((res) => {
const appStore = useAppStore()
setStorage(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
setStorage(appStore.getRefreshToken, res.data.refresh_token)
const authStore = useAuthStoreWithOut()
authStore.setToken(`${res.data.token_type} ${res.data.access_token}`)
authStore.setRefreshToken(res.data.refresh_token)
})
}
return response.data
} else if (code === unauthorized_code) {
// 因token无效token过期导致
refreshToken().then((res) => {
const appStore = useAppStore()
setStorage(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
setStorage(appStore.getRefreshToken, res.data.refresh_token)
const authStore = useAuthStoreWithOut()
authStore.setToken(`${res.data.token_type} ${res.data.access_token}`)
authStore.setRefreshToken(res.data.refresh_token)
ElMessage.error('操作失败,请重试')
})
} else {
@ -108,7 +104,7 @@ service.interceptors.response.use(
(error: AxiosError) => {
console.log('err', error)
let { message } = error
const authStore = useAuthStore()
const authStore = useAuthStoreWithOut()
const status = error.response?.status
switch (status) {
case 400:
@ -117,7 +113,7 @@ service.interceptors.response.use(
case 401:
// 强制要求重新登录因账号已冻结账号已过期手机号码错误刷新token无效等问题导致
authStore.logout()
message = '认证已过期,请重新登录'
message = '认证已失效,请重新登录'
break
case 403:
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
@ -158,8 +154,8 @@ service.interceptors.response.use(
// 刷新Token
const refreshToken = (): Promise<IResponse> => {
const appStore = useAppStore()
const data = getStorage(appStore.getRefreshToken)
const authStore = useAuthStoreWithOut()
const data = authStore.getRefreshToken
return request.post({ url: '/auth/token/refresh', data })
}

View File

@ -5,11 +5,11 @@ import { isArray } from '@/utils/is'
import { useAuthStoreWithOut } from '@/store/modules/auth'
const { t } = useI18n()
const authStore = useAuthStoreWithOut()
// 全部权限
const all_permission = ['*.*.*']
const hasPermission = (value: string | string[]): boolean => {
const authStore = useAuthStoreWithOut()
const permissions = authStore.getPermissions
if (!value) {
throw new Error(t('permission.hasPermission'))

View File

@ -0,0 +1,47 @@
import { ref } from 'vue'
const useClipboard = () => {
const copied = ref(false)
const text = ref('')
const isSupported = ref(false)
if (!navigator.clipboard && !document.execCommand) {
isSupported.value = false
} else {
isSupported.value = true
}
const copy = (str: string) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(str).then(() => {
text.value = str
copied.value = true
resetCopied()
})
return
}
const input = document.createElement('input')
input.setAttribute('readonly', 'readonly')
input.setAttribute('value', str)
document.body.appendChild(input)
input.select()
input.setSelectionRange(0, 9999)
if (document.execCommand('copy')) {
text.value = str
document.execCommand('copy')
copied.value = true
resetCopied()
}
document.body.removeChild(input)
}
const resetCopied = () => {
setTimeout(() => {
copied.value = false
}, 1500)
}
return { copy, text, copied, isSupported }
}
export { useClipboard }

View File

@ -79,19 +79,14 @@ const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
for (let i = 0; i < length; i++) {
const schemaItem = crudSchema[i]
// 判断是否隐藏
if (!schemaItem?.search?.hidden) {
const searchSchemaItem = {
component: schemaItem?.search?.component || 'Input',
...schemaItem.search,
field: schemaItem.field,
label: schemaItem.label
}
// 删除不必要的字段
delete searchSchemaItem.hidden
searchSchema.push(searchSchemaItem)
const searchSchemaItem = {
component: schemaItem?.search?.component || 'Input',
...schemaItem.search,
field: schemaItem.field,
label: schemaItem.search?.label || schemaItem.label
}
searchSchema.push(searchSchemaItem)
}
return searchSchema
@ -103,8 +98,8 @@ const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
conversion: (schema: CrudSchema) => {
if (!schema?.table?.hidden) {
return {
...schema.table,
...schema
...schema,
...schema.table
}
}
}
@ -127,19 +122,14 @@ const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
for (let i = 0; i < length; i++) {
const formItem = crudSchema[i]
// 判断是否隐藏
if (!formItem?.form?.hidden) {
const formSchemaItem = {
component: formItem?.form?.component || 'Input',
...formItem.form,
field: formItem.field,
label: formItem.label
}
// 删除不必要的字段
delete formSchemaItem.hidden
formSchema.push(formSchemaItem)
const formSchemaItem = {
component: formItem?.form?.component || 'Input',
...formItem.form,
field: formItem.field,
label: formItem.form?.label || formItem.label
}
formSchema.push(formSchemaItem)
}
return formSchema

View File

@ -2,6 +2,7 @@ import type { Form, FormExpose } from '@/components/Form'
import type { ElForm, ElFormItem } from 'element-plus'
import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps, FormProps } from '@/components/Form'
import { isEmptyVal, isObject } from '@/utils/is'
export const useForm = () => {
// From实例
@ -93,9 +94,27 @@ export const useForm = () => {
* @description
* @returns form data
*/
getFormData: async <T = Recordable>(): Promise<T> => {
getFormData: async <T = Recordable>(filterEmptyVal = true): Promise<T> => {
const form = await getForm()
return form?.formModel as T
const model = form?.formModel as any
if (filterEmptyVal) {
// 使用reduce过滤空值并返回一个新对象
return Object.keys(model).reduce((prev, next) => {
const value = model[next]
if (!isEmptyVal(value)) {
if (isObject(value)) {
if (Object.keys(value).length > 0) {
prev[next] = value
}
} else {
prev[next] = value
}
}
return prev
}, {}) as T
} else {
return model as T
}
},
/**

View File

@ -0,0 +1,21 @@
import { ref, onBeforeUnmount } from 'vue'
const useNetwork = () => {
const online = ref(true)
const updateNetwork = () => {
online.value = navigator.onLine
}
window.addEventListener('online', updateNetwork)
window.addEventListener('offline', updateNetwork)
onBeforeUnmount(() => {
window.removeEventListener('online', updateNetwork)
window.removeEventListener('offline', updateNetwork)
})
return { online }
}
export { useNetwork }

View File

@ -1,13 +1,15 @@
import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
export const usePageLoading = () => {
const loadStart = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(true)
}
const loadDone = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(false)
}

View File

@ -4,7 +4,7 @@ const getValueType = (value: any) => {
return type.slice(8, -1)
}
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'localStorage') => {
const setStorage = (key: string, value: any) => {
const valueType = getValueType(value)
window[type].setItem(key, JSON.stringify({ type: valueType, value }))

View File

@ -3,10 +3,10 @@ import { isString } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
const appStore = useAppStoreWithOut()
export const useTitle = (newTitle?: string) => {
const { t } = useI18n()
const appStore = useAppStoreWithOut()
const title = ref(
newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
)

View File

@ -71,8 +71,14 @@ export default defineComponent({
.@{prefix-cls} {
background-color: var(--app-content-bg-color);
:deep(.@{elNamespace}-scrollbar__view) {
height: 100% !important;
.@{prefix-cls}-content-scrollbar {
& > :deep(.el-scrollbar__wrap) {
& > .@{elNamespace}-scrollbar__view {
display: flex;
height: 100% !important;
flex-direction: column;
}
}
}
}
</style>

View File

@ -6,10 +6,6 @@ import { computed } from 'vue'
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const fixedHeader = computed(() => appStore.getFixedHeader)
const footer = computed(() => appStore.getFooter)
const tagsViewStore = useTagsViewStore()
@ -17,39 +13,12 @@ const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews
})
const tagsView = computed(() => appStore.getTagsView)
</script>
<template>
<section
:class="[
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color-new)] dark:bg-[var(--el-bg-color)]',
{
'!min-h-[calc(100%-var(--app-footer-height))]':
(fixedHeader &&
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
footer) ||
(!tagsView && layout === 'top' && footer),
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
tagsView && layout === 'top' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
!fixedHeader && layout === 'classic' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
!fixedHeader && layout === 'topLeft' && footer,
// '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height)-var(--top-tool-height))]':
// !fixedHeader && layout === 'top' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
fixedHeader && layout === 'cutMenu' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
!fixedHeader && layout === 'cutMenu' && footer
}
'flex-1 p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]'
]"
>
<router-view>

View File

@ -42,8 +42,7 @@ export default defineComponent({
id={`${variables.namespace}-tool-header`}
class={[
prefixCls,
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between',
'dark:bg-[var(--el-bg-color)]'
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
]}
>
{layout.value !== 'top' ? (

View File

@ -195,8 +195,8 @@ export const useRenderLayout = () => {
`${prefixCls}-content`,
'w-full',
{
'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value,
'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value
'h-[calc(100%-var(--top-tool-height))]': !fixedHeader.value,
'h-[calc(100%-var(--tags-view-height)-var(--top-tool-height))]': fixedHeader.value
}
]}
>

View File

@ -50,6 +50,8 @@ export default {
notSpace: 'Spaces are not allowed',
notSpecialCharacters: 'Special characters are not allowed',
isEqual: 'The two are not equal',
// 列设置
setting: 'Setting',
selectAll: 'Select all',
SerialNumberColumn: 'Index column'
},
@ -186,7 +188,15 @@ export default {
permission: 'Permission test page',
function: 'Function',
multipleTabs: 'Multiple tabs',
details: 'Details'
details: 'Details',
iconPicker: 'Icon picker',
request: 'Request',
waterfall: 'Waterfall',
imageCropping: 'Image cropping',
videoPlayer: 'Video player',
// 表格视频预览
tableVideoPreview: 'Table video preview',
cardTable: 'Card table'
},
permission: {
hasPermission: 'Please set the operation permission value'
@ -332,7 +342,8 @@ export default {
lazyLoad: 'Lazy load',
upload: 'Upload',
// 用户头像
userAvatar: 'User avatar'
userAvatar: 'User avatar',
iconPicker: 'Icon picker'
},
guideDemo: {
guide: 'Guide',
@ -461,7 +472,9 @@ export default {
fixedHeaderOrAuto: 'Fixed header or auto',
getSelections: 'Get selections',
preview: 'Preview',
showOrHiddenSortable: 'Show or hidden sortable'
showOrHiddenSortable: 'Show or hidden sortable',
videoPreview: 'Video preview',
cardTable: 'Card table'
},
richText: {
richText: 'Rich text',

View File

@ -50,6 +50,7 @@ export default {
notSpace: '不能包含空格',
notSpecialCharacters: '不能包含特殊字符',
isEqual: '两次输入不一致',
setting: '设置',
selectAll: '全选',
SerialNumberColumn: '序号列'
},
@ -84,7 +85,7 @@ export default {
sizeIcon: '尺寸图标',
localeIcon: '多语言图标',
tagsView: '标签页',
logo: '标志',
logo: 'Logo',
greyMode: '灰色模式',
fixedHeader: '固定头部',
headerTheme: '头部主题',
@ -184,7 +185,14 @@ export default {
permission: '权限测试页',
function: '功能',
multipleTabs: '多开标签页',
details: '详情页'
details: '详情页',
iconPicker: '图标选择器',
request: '请求',
waterfall: '瀑布流',
imageCropping: '图片裁剪',
videoPlayer: '视频播放器',
tableVideoPreview: '表格视频预览',
cardTable: '卡片表格'
},
permission: {
hasPermission: '请设置操作权限值'
@ -327,7 +335,8 @@ export default {
customContent: '自定义内容',
lazyLoad: '懒加载',
upload: '上传',
userAvatar: '用户头像'
userAvatar: '用户头像',
iconPicker: '图标选择器'
},
guideDemo: {
guide: '引导页',
@ -454,7 +463,9 @@ export default {
fixedHeaderOrAuto: '固定头部/自动',
getSelections: '获取多选数据',
preview: '封面',
showOrHiddenSortable: '显示/隐藏排序'
showOrHiddenSortable: '显示/隐藏排序',
videoPreview: '视频预览',
cardTable: '卡片表格'
},
richText: {
richText: '富文本',
@ -530,7 +541,7 @@ export default {
menu: {
menuName: '菜单名称',
icon: '图标',
permission: '权限标识',
permission: '按钮权限',
component: '组件',
path: '路径',
status: '状态',

View File

@ -1,3 +1,5 @@
import 'vue/jsx'
// 引入windi css
import '@/plugins/unocss'

View File

@ -1,6 +1,5 @@
import { hasRoute } from './router'
import router from './router'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useStorage } from '@/hooks/web/useStorage'
import type { RouteRecordRaw } from 'vue-router'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
@ -9,23 +8,19 @@ import { usePageLoading } from '@/hooks/web/usePageLoading'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { getRoleMenusApi } from '@/api/login'
const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut()
const authStore = useAuthStoreWithOut()
const { getStorage, setStorage } = useStorage()
const { start, done } = useNProgress()
const { loadStart, loadDone } = usePageLoading()
const whiteList = ['/login'] // 不重定向白名单
const whiteList = ['/login', '/docs/privacy', '/docs/agreement'] // 不重定向白名单
router.beforeEach(async (to, from, next) => {
start()
loadStart()
if (getStorage(appStore.getToken)) {
const permissionStore = usePermissionStoreWithOut()
const authStore = useAuthStoreWithOut()
if (authStore.getToken) {
if (to.path === '/login') {
next({ path: '/' })
} else if (to.path === '/reset/password') {
@ -35,6 +30,9 @@ router.beforeEach(async (to, from, next) => {
await authStore.setUserInfo()
}
if (permissionStore.getIsAddRouters) {
if (!hasRoute(to.path)) {
authStore.logout('认证已过期,请重新登录!')
}
next()
return
}
@ -42,7 +40,6 @@ router.beforeEach(async (to, from, next) => {
// 开发者可根据实际情况进行修改
const res = await getRoleMenusApi()
const routers = res.data || []
setStorage('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
router.addRoute(route as RouteRecordRaw) // 动态添加可访问路由表

View File

@ -12,7 +12,13 @@ export const setupElementPlus = (app: App<Element>) => {
app.use(plugin)
})
// 为了开发环境启动更快,一次性引入所有样式
if (import.meta.env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'true') {
import('element-plus/dist/index.css')
return
}
components.forEach((component) => {
app.component(component.name, component)
app.component(component.name!, component)
})
}

View File

@ -1,3 +1 @@
import 'virtual:svg-icons-register'
import '@purge-icons/generated'

View File

@ -70,6 +70,37 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
noTagsView: true
}
},
{
path: '/docs',
name: 'Docs',
meta: {
hidden: true,
title: '在线文档',
noTagsView: true
},
children: [
{
path: 'privacy',
name: 'Privacy',
component: () => import('@/views/Vadmin/Docs/Privacy.vue'),
meta: {
hidden: true,
title: '隐私政策',
noTagsView: true
}
},
{
path: 'agreement',
name: 'Agreement',
component: () => import('@/views/Vadmin/Docs/Agreement.vue'),
meta: {
hidden: true,
title: '用户协议',
noTagsView: true
}
}
]
},
{
path: '/404',
component: () => import('@/views/Error/404.vue'),
@ -115,7 +146,17 @@ const router = createRouter({
})
export const resetRouter = (): void => {
const resetWhiteNameList = ['Login', 'NoFind', 'Root']
const resetWhiteNameList = [
'Login',
'NoFind',
'Root',
'ResetPassword',
'Redirect',
'Home',
'Docs',
'Privacy',
'Agreement'
]
router.getRoutes().forEach((route) => {
const { name } = route
if (name && !resetWhiteNameList.includes(name as string)) {
@ -124,6 +165,12 @@ export const resetRouter = (): void => {
})
}
// 判断是否已经有某个路径的路由配置
export const hasRoute = (path: string): boolean => {
const resolvedRoute = router.resolve(path)
return resolvedRoute.matched.length > 0
}
export const setupRouter = (app: App<Element>) => {
app.use(router)
}

View File

@ -1,10 +1,12 @@
import type { App } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// pinia-plugin-persistedstate 持久化存储官方文档https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/
const store = createPinia()
store.use(piniaPersist)
store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => {
app.use(store)

View File

@ -2,9 +2,10 @@ import { defineStore } from 'pinia'
import { store } from '../index'
import { setCssVar, humpToUnderline } from '@/utils'
import { ElMessage, ComponentSize } from 'element-plus'
import { useStorage } from '@/hooks/web/useStorage'
const { getStorage, setStorage } = useStorage()
import { colorIsDark, hexToRGB, lighten, mix } from '@/utils/color'
import { useCssVar } from '@vueuse/core'
import { unref } from 'vue'
import { useDark } from '@vueuse/core'
interface AppState {
breadcrumb: boolean
@ -25,7 +26,6 @@ interface AppState {
pageLoading: boolean
layout: LayoutType
title: string
userInfo: string
isDark: boolean
currentSize: ComponentSize
sizeMap: ComponentSize[]
@ -34,8 +34,6 @@ interface AppState {
theme: ThemeTypes
fixedMenu: boolean
token: string
refreshToken: string
logoImage: string
footerContent: string
icpNumber: string
@ -44,7 +42,6 @@ interface AppState {
export const useAppStore = defineStore('app', {
state: (): AppState => {
return {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其它项目冲突
sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题
@ -63,14 +60,14 @@ export const useAppStore = defineStore('app', {
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: getStorage('dynamicRouter'), // 是否动态路由
serverDynamicRouter: getStorage('serverDynamicRouter'), // 是否服务端渲染动态路由
fixedMenu: getStorage('fixedMenu'), // 是否固定菜单
dynamicRouter: true, // 是否动态路由
serverDynamicRouter: true, // 是否服务端渲染动态路由
fixedMenu: false, // 是否固定菜单
layout: getStorage('layout') || 'classic', // layout布局
isDark: getStorage('isDark'), // 是否是暗黑模式
currentSize: getStorage('default') || 'default', // 组件尺寸
theme: getStorage('theme') || {
layout: 'classic', // layout布局
isDark: false, // 是否是暗黑模式
currentSize: 'default', // 组件尺寸
theme: {
// 主题色
elColorPrimary: '#409eff',
// 左侧菜单边框颜色
@ -101,8 +98,6 @@ export const useAppStore = defineStore('app', {
topToolBorderColor: '#eee'
},
token: 'Token', // 存储Token字段
refreshToken: 'RefreshToken', // 存储刷新Token字段
logoImage: '', // logo图片
footerContent: '', // 页脚内容
icpNumber: '' // 备案号
@ -166,9 +161,6 @@ export const useAppStore = defineStore('app', {
getTitle(): string {
return this.title
},
getUserInfo(): string {
return this.userInfo
},
getIsDark(): boolean {
return this.isDark
},
@ -191,12 +183,6 @@ export const useAppStore = defineStore('app', {
getLogoImage(): string {
return this.logoImage
},
getToken(): string {
return this.token
},
getRefreshToken(): string {
return this.refreshToken
},
getFooterContent(): string {
return this.footerContent
},
@ -245,15 +231,12 @@ export const useAppStore = defineStore('app', {
this.greyMode = greyMode
},
setDynamicRouter(dynamicRouter: boolean) {
setStorage('dynamicRouter', dynamicRouter)
this.dynamicRouter = dynamicRouter
},
setServerDynamicRouter(serverDynamicRouter: boolean) {
setStorage('serverDynamicRouter', serverDynamicRouter)
this.serverDynamicRouter = serverDynamicRouter
},
setFixedMenu(fixedMenu: boolean) {
setStorage('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu
},
setPageLoading(pageLoading: boolean) {
@ -265,7 +248,6 @@ export const useAppStore = defineStore('app', {
return
}
this.layout = layout
setStorage('layout', this.layout)
},
setTitle(title: string) {
this.title = title
@ -279,23 +261,22 @@ export const useAppStore = defineStore('app', {
document.documentElement.classList.add('light')
document.documentElement.classList.remove('dark')
}
setStorage('isDark', this.isDark)
this.setPrimaryLight()
},
setCurrentSize(currentSize: ComponentSize) {
this.currentSize = currentSize
setStorage('currentSize', this.currentSize)
},
setMobile(mobile: boolean) {
this.mobile = mobile
},
setTheme(theme: ThemeTypes) {
this.theme = Object.assign(this.theme, theme)
setStorage('theme', this.theme)
},
setCssVarTheme() {
for (const key in this.theme) {
setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
}
this.setPrimaryLight()
},
setFooter(footer: boolean) {
this.footer = footer
@ -309,8 +290,75 @@ export const useAppStore = defineStore('app', {
},
setIcpNumber(icpNumber: string) {
this.icpNumber = icpNumber
},
setPrimaryLight() {
if (this.theme.elColorPrimary) {
const elColorPrimary = this.theme.elColorPrimary
const color = this.isDark ? '#000000' : '#ffffff'
const lightList = [3, 5, 7, 8, 9]
lightList.forEach((v) => {
setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10))
})
setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2))
}
},
setMenuTheme(color: string) {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
// 左侧菜单边框颜色
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
// 左侧菜单背景颜色
leftMenuBgColor: color,
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单字体颜色
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo字体颜色
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo边框颜色
logoBorderColor: isDarkColor ? color : '#eee'
}
this.setTheme(theme)
this.setCssVarTheme()
},
setHeaderTheme(color: string) {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
this.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (this.getLayout === 'top') {
this.setMenuTheme(color)
}
},
initTheme() {
const isDark = useDark({
valueDark: 'dark',
valueLight: 'light'
})
isDark.value = this.getIsDark
}
}
},
persist: true
})
export const useAppStoreWithOut = () => {

View File

@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
import { store } from '../index'
import { UserLoginType } from '@/api/login/types'
import { loginApi } from '@/api/login'
import { useAppStore } from '@/store/modules/app'
import { useStorage } from '@/hooks/web/useStorage'
import { getCurrentAdminUserInfo } from '@/api/vadmin/auth/user'
import { resetRouter } from '@/router'
@ -10,7 +9,7 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
import router from '@/router'
import { ElMessage } from 'element-plus'
const { setStorage, clear } = useStorage()
const { clear } = useStorage()
export interface UserState {
id?: number
@ -32,6 +31,9 @@ export interface AuthState {
isUser: boolean // 是否已经登录并获取到用户信息
roles: string[] // 当前用户角色 role_key 列表
permissions: string[] // 当前用户权限列表
tokenKey: string // 提交认证请求时,设置的 header key
token: string // 认证 token
refreshToken: string // 刷新 token
}
export const useAuthStore = defineStore('auth', {
@ -40,10 +42,22 @@ export const useAuthStore = defineStore('auth', {
user: {},
roles: [],
permissions: [],
isUser: false
isUser: false,
tokenKey: 'Authorization',
token: '',
refreshToken: ''
}
},
getters: {
getTokenKey(): string {
return this.tokenKey
},
getToken(): string {
return this.token
},
getRefreshToken(): string {
return this.refreshToken
},
getUser(): UserState {
return this.user
},
@ -58,24 +72,34 @@ export const useAuthStore = defineStore('auth', {
}
},
actions: {
setToken(token: string) {
this.token = token
},
setRefreshToken(refreshToken: string) {
this.refreshToken = refreshToken
},
async login(formData: UserLoginType) {
formData.platform = '0'
const res = await loginApi(formData)
if (res) {
const appStore = useAppStore()
setStorage(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
setStorage(appStore.getRefreshToken, res.data.refresh_token)
this.token = `${res.data.token_type} ${res.data.access_token}`
this.refreshToken = res.data.refresh_token
// 获取当前登录用户的信息
await this.setUserInfo()
}
return res
},
logout(message?: string) {
clear()
reset() {
this.user = {}
this.roles = []
this.permissions = []
this.isUser = false
this.token = ''
this.refreshToken = ''
},
logout(message?: string) {
clear()
this.reset()
const tagsViewStore = useTagsViewStore()
tagsViewStore.delAllViews()
resetRouter()
@ -105,7 +129,8 @@ export const useAuthStore = defineStore('auth', {
})
this.permissions = res.data.permissions
}
}
},
persist: true
})
export const useAuthStoreWithOut = () => {

View File

@ -10,7 +10,11 @@ export const useDictStore = defineStore('dict', {
state: (): DictState => ({
dictObj: {}
}),
getters: {},
getters: {
getDictObjData(): Recordable {
return this.dictObj
}
},
actions: {
async getDictObj(dictTypes: string[]) {
const result: Recordable = {}
@ -34,7 +38,8 @@ export const useDictStore = defineStore('dict', {
}
return result
}
}
},
persist: true
})
export const useDictStoreWithOut = () => {

View File

@ -5,7 +5,7 @@ import en from 'element-plus/es/locale/lang/en'
import { useStorage } from '@/hooks/web/useStorage'
import { LocaleDropdownType } from '@/components/LocaleDropdown'
const { getStorage, setStorage } = useStorage()
const { getStorage, setStorage } = useStorage('localStorage')
const elLocaleMap = {
'zh-CN': zhCn,
@ -51,7 +51,8 @@ export const useLocaleStore = defineStore('locales', {
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
setStorage('lang', localeMap?.lang)
}
}
},
persist: true
})
export const useLocaleStoreWithOut = () => {

View File

@ -40,10 +40,7 @@ export const useLockStore = defineStore('lock', {
}
}
},
persist: {
enabled: true,
strategies: [{ key: 'lock', storage: localStorage }]
}
persist: true
})
export const useLockStoreWithOut = () => {

View File

@ -60,6 +60,9 @@ export const usePermissionStore = defineStore('permission', {
setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
this.menuTabRouters = routers
}
},
persist: {
paths: ['routers', 'addRouters', 'menuTabRouters']
}
})

View File

@ -4,12 +4,7 @@ import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia'
import { store } from '../index'
import { findIndex } from '@/utils'
import { useStorage } from '@/hooks/web/useStorage'
import { useAppStoreWithOut } from './app'
const appStore = useAppStoreWithOut()
const { getStorage } = useStorage()
import { useAuthStoreWithOut } from './auth'
export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[]
@ -95,8 +90,9 @@ export const useTagsViewStore = defineStore('tagsView', {
},
// 删除所有tag
delAllVisitedViews() {
const authStore = useAuthStoreWithOut()
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = getStorage(appStore.getUserInfo)
this.visitedViews = authStore.getUser
? this.visitedViews.filter((tag) => tag?.meta?.affix)
: []
},
@ -157,7 +153,8 @@ export const useTagsViewStore = defineStore('tagsView', {
}
}
}
}
},
persist: false
})
export const useTagsViewStoreWithOut = () => {

View File

@ -151,3 +151,22 @@ const subtractLight = (color: string, amount: number) => {
const c = cc < 0 ? 0 : cc
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
}
/**
* Mixes two colors.
*
* @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`.
* @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`.
* @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1.
* @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`.
*/
export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
let color = '#'
for (let i = 0; i <= 2; i++) {
const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
const c = Math.round(c1 * weight + c2 * (1 - weight))
color += c.toString(16).padStart(2, '0')
}
return color
}

View File

@ -1,5 +1,3 @@
// import type { Plugin } from 'vue'
/**
*
* @param component
@ -47,6 +45,10 @@ export const setCssVar = (prop: string, val: any, dom = document.documentElement
dom.style.setProperty(prop, val)
}
export const getCssVar = (prop: string, dom = document.documentElement) => {
return getComputedStyle(dom).getPropertyValue(prop)
}
/**
*
* @param {Array} ary
@ -123,6 +125,17 @@ export function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}
/**
* formData
*/
export function objToFormData(obj: Recordable) {
const formData = new FormData()
Object.keys(obj).forEach((key) => {
formData.append(key, obj[key])
})
return formData
}
// 根据当前时间获取祝福语
export const getGreeting = (): string => {
const now = new Date()

View File

@ -1,12 +1,11 @@
import { createTypes, VueTypesInterface, VueTypeValidableDef } from 'vue-types'
import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types'
import { CSSProperties } from 'vue'
// 自定义扩展vue-types
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const propTypes = createTypes({
const newPropTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
@ -15,15 +14,12 @@ const propTypes = createTypes({
integer: undefined
}) as PropTypes
// 需要自定义扩展的类型
// see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
propTypes.extend([
{
name: 'style',
getter: true,
type: [String, Object],
default: undefined
class propTypes extends newPropTypes {
static get style() {
return toValidableType('style', {
type: [String, Object]
})
}
])
}
export { propTypes }

View File

@ -3,6 +3,7 @@ import { ref } from 'vue'
import Finance from './components/Finance.vue'
import { ElTabs, ElTabPane } from 'element-plus'
import User from './components/User.vue'
import { ContentWrap } from '@/components/ContentWrap'
defineOptions({
name: 'DashboardAnalysis'
@ -12,7 +13,7 @@ const activeName = ref('user')
</script>
<template>
<div class="p-5">
<ContentWrap>
<ElTabs v-model="activeName">
<ElTabPane label="财务分析" name="finance" :lazy="true">
<Finance />
@ -21,7 +22,7 @@ const activeName = ref('user')
<User />
</ElTabPane>
</ElTabs>
</div>
</ContentWrap>
</template>
<style scoped></style>

View File

@ -155,12 +155,12 @@ initMap()
background-color: #f13737;
box-shadow: 0px 0px 15px #f61212;
border-radius: 50%;
-webkit-animation-name: 'alarmDeviceBreath'; /*动画属性名也就是我们前面keyframes定义的动画名*/
-webkit-animation-duration: 1s; /*动画持续时间*/
-webkit-animation-timing-function: ease; /*动画频率和transition-timing-function是一样的*/
-webkit-animation-delay: 0s; /*动画延迟时间*/
-webkit-animation-iteration-count: infinite; /*定义循环资料infinite为无限次*/
-webkit-animation-direction: alternate; /*定义动画方式*/
--webkit-animation-name: 'alarmDeviceBreath'; /*动画属性名也就是我们前面keyframes定义的动画名*/
--webkit-animation-duration: 1s; /*动画持续时间*/
--webkit-animation-timing-function: ease; /*动画频率和transition-timing-function是一样的*/
--webkit-animation-delay: 0s; /*动画延迟时间*/
--webkit-animation-iteration-count: infinite; /*定义循环资料infinite为无限次*/
--webkit-animation-direction: alternate; /*定义动画方式*/
}
</style>

View File

@ -3,14 +3,14 @@ import { ElCard, ElRow, ElCol, ElTabs, ElTabPane, ElAvatar } from 'element-plus'
import { computed, ref } from 'vue'
import InfoWrite from './components/InfoWrite.vue'
import PasswordWrite from './components/PasswordWrite.vue'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAuthStore } from '@/store/modules/auth'
import avatar from '@/assets/imgs/avatar.jpg'
import { selectDictLabel, DictDetail } from '@/utils/dict'
import { useDictStore } from '@/store/modules/dict'
const activeName = ref('info')
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
let genderOptions = ref<DictDetail[]>([])

View File

@ -3,13 +3,14 @@ import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { ElButton, ElMessage } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth'
import { ElMessage } from 'element-plus'
import { postCurrentUserUpdateInfo } from '@/api/vadmin/auth/user'
import { BaseButton } from '@/components/Button'
const { required, isTelephone } = useValidator()
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const formSchema = reactive<FormSchema[]>([
{
@ -92,9 +93,9 @@ const formSchema = reactive<FormSchema[]>([
return (
<>
<div class="w-[50%]">
<ElButton loading={loading.value} type="primary" class="w-[100%]" onClick={save}>
<BaseButton loading={loading.value} type="primary" class="w-[100%]" onClick={save}>
保存
</ElButton>
</BaseButton>
</div>
</>
)

View File

@ -3,13 +3,14 @@ import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { ElButton, ElMessage } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth'
import { ElMessage } from 'element-plus'
import { postCurrentUserResetPassword } from '@/api/vadmin/auth/user'
import { BaseButton } from '@/components/Button'
const { required } = useValidator()
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const formSchema = reactive<FormSchema[]>([
{
@ -57,9 +58,9 @@ const formSchema = reactive<FormSchema[]>([
return (
<>
<div class="w-[50%]">
<ElButton loading={loading.value} type="primary" class="w-[100%]" onClick={save}>
<BaseButton loading={loading.value} type="primary" class="w-[100%]" onClick={save}>
保存
</ElButton>
</BaseButton>
</div>
</>
)
@ -101,7 +102,8 @@ const save = async () => {
const res = await postCurrentUserResetPassword(formData)
if (res) {
elForm?.resetFields()
ElMessage.success('保存成功')
authStore.logout()
ElMessage.warning('请重新登录')
}
} finally {
loading.value = false

View File

@ -9,6 +9,7 @@ import { useDesign } from '@/hooks/web/useDesign'
import { ref } from 'vue'
import { ElScrollbar } from 'element-plus'
import { computed } from 'vue'
import { ElButton } from 'element-plus'
const { getPrefixCls } = useDesign()
@ -28,6 +29,11 @@ const toTelephoneLogin = () => {
const toPasswordLogin = () => {
isPasswordLogin.value = true
}
const icpNumber = computed(() => appStore.getIcpNumber)
const toICO = () => {
window.open('https://beian.miit.gov.cn/#/Integrated/index')
}
</script>
<template>
@ -88,6 +94,9 @@ const toPasswordLogin = () => {
/>
</div>
</Transition>
<div class="text-14px text-white font-normal absolute bottom-5 right-10">
<ElButton type="info" link @click="toICO">{{ icpNumber }}</ElButton>
</div>
</div>
</div>
</ElScrollbar>

View File

@ -2,18 +2,18 @@
import { reactive, ref, watch } from 'vue'
import { Form } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { ElButton, ElCheckbox, ElLink } from 'element-plus'
import { ElCheckbox, ElLink } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { getRoleMenusApi } from '@/api/login'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAuthStore } from '@/store/modules/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { UserLoginType } from '@/api/login/types'
import { useValidator } from '@/hooks/web/useValidator'
import { useStorage } from '@/hooks/web/useStorage'
import { FormSchema } from '@/components/Form'
import { Icon } from '@/components/Icon'
import { BaseButton } from '@/components/Button'
const emit = defineEmits(['to-telephone'])
@ -21,10 +21,9 @@ const { required } = useValidator()
const permissionStore = usePermissionStore()
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const { currentRoute, addRoute, push } = useRouter()
const { setStorage } = useStorage()
const { t } = useI18n()
@ -122,14 +121,19 @@ const schema = reactive<FormSchema[]>([
return (
<>
<div class="w-[100%]">
<ElButton loading={loading.value} type="primary" class="w-[100%]" onClick={signIn}>
<BaseButton
loading={loading.value}
type="primary"
class="w-[100%]"
onClick={signIn}
>
{t('login.login')}
</ElButton>
</BaseButton>
</div>
<div class="w-[100%] mt-15px">
<ElButton class="w-[100%]" onClick={toTelephoneLogin}>
<BaseButton class="w-[100%]" onClick={toTelephoneLogin}>
{t('login.smsLogin')}
</ElButton>
</BaseButton>
</div>
</>
)
@ -241,7 +245,6 @@ const getMenu = async () => {
const res = await getRoleMenusApi()
if (res) {
const routers = res.data || []
setStorage('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访

View File

@ -3,16 +3,16 @@ import { Form } from '@/components/Form'
import { reactive, ref, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useForm } from '@/hooks/web/useForm'
import { ElButton, ElInput, FormRules, ElDivider, ElMessage } from 'element-plus'
import { ElInput, FormRules, ElDivider, ElMessage } from 'element-plus'
import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/components/Form'
import { postSMSCodeApi } from '@/api/login'
import { UserLoginType } from '@/api/login/types'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAuthStore } from '@/store/modules/auth'
import { RouteLocationNormalizedLoaded, useRouter, RouteRecordRaw } from 'vue-router'
import { getRoleMenusApi } from '@/api/login'
import { useStorage } from '@/hooks/web/useStorage'
import { usePermissionStore } from '@/store/modules/permission'
import { BaseButton } from '@/components/Button'
const emit = defineEmits(['to-password'])
@ -22,8 +22,7 @@ const { t } = useI18n()
const { required } = useValidator()
const { currentRoute, addRoute, push } = useRouter()
const permissionStore = usePermissionStore()
const authStore = useAuthStoreWithOut()
const { setStorage } = useStorage()
const authStore = useAuthStore()
const schema = reactive<FormSchema[]>([
{
@ -74,13 +73,13 @@ const schema = reactive<FormSchema[]>([
<>
<ElDivider direction="vertical" />
{SMSCodeStatus.value ? (
<ElButton type="primary" link onClick={getSMSCode}>
<BaseButton type="primary" link onClick={getSMSCode}>
{t('login.getSMSCode')}
</ElButton>
</BaseButton>
) : (
<ElButton type="primary" disabled={!SMSCodeStatus.value} link>
<BaseButton type="primary" disabled={!SMSCodeStatus.value} link>
{SMSCodeNumber.value + t('login.SMSCodeRetry')}
</ElButton>
</BaseButton>
)}
</>
)
@ -110,19 +109,19 @@ const schema = reactive<FormSchema[]>([
return (
<div class="w-[100%]">
<div class="w-[100%]">
<ElButton
<BaseButton
type="primary"
class="w-[100%]"
loading={loading.value}
onClick={telephoneCodeLogin}
>
{t('login.login')}
</ElButton>
</BaseButton>
</div>
<div class="w-[100%] mt-15px">
<ElButton class="w-[100%]" onClick={toPasswordLogin}>
<BaseButton class="w-[100%]" onClick={toPasswordLogin}>
{t('login.passwordLogin')}
</ElButton>
</BaseButton>
</div>
</div>
)
@ -216,7 +215,6 @@ const getMenu = async () => {
const res = await getRoleMenusApi()
if (res) {
const routers = res.data || []
setStorage('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访

View File

@ -3,21 +3,19 @@ import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { computed, reactive, ref, watch } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { ElButton, ElMessage } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth'
import { ElMessage } from 'element-plus'
import { postCurrentUserResetPassword } from '@/api/vadmin/auth/user'
import { getRoleMenusApi } from '@/api/login'
import { useStorage } from '@/hooks/web/useStorage'
import { usePermissionStore } from '@/store/modules/permission'
import { RouteLocationNormalizedLoaded, RouteRecordRaw, useRouter } from 'vue-router'
import { useAppStore } from '@/store/modules/app'
import { Footer } from '@/components/Footer'
const { required } = useValidator()
const { setStorage } = useStorage()
const { addRoute, push, currentRoute } = useRouter()
const authStore = useAuthStoreWithOut()
const authStore = useAuthStore()
const appStore = useAppStore()
const permissionStore = usePermissionStore()
@ -112,7 +110,6 @@ const getMenu = async () => {
const res = await getRoleMenusApi()
if (res) {
const routers = res.data || []
setStorage('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
@ -137,9 +134,9 @@ const getMenu = async () => {
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
/>
<div class="w-[100%]">
<ElButton :loading="loading" type="primary" class="w-[100%]" @click="save">
<BaseButton :loading="loading" type="primary" class="w-[100%]" @click="save">
重置密码
</ElButton>
</BaseButton>
</div>
</div>

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