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 */.idea
dvadmin-doc/docs/.vuepress/dist

126
README.md
View File

@ -21,10 +21,11 @@
Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 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) - 后端采用现代、快速(高性能) [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)为主要技术开发; - 移动端采用 [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/) 命令行应用,简单化数据初始化,数据表模型迁移等操作; - 后端加入 [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/),支持多终端认证系统。 - 权限认证使用[(哈希)密码和 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/) 数据库异步操作。 - 已加入常见的 [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/):国内首个国密前后端分离快速开发平台 [小诺开源技术 (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 PC端演示地址https://kinit.ktianc.top
@ -58,9 +80,71 @@ PC端演示地址https://kinit.ktianc.top
</div> </div>
管理员账户:
- 账号15020221010 - 账号15020221010
- 密码kinit2022 - 密码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 gitee地址(主推)https://gitee.com/ktianc/kinit
@ -71,7 +155,9 @@ github地址https://github.com/vvandk/kinit
- [x] 菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。 - [x] 菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
- [x] 角色管理:角色菜单权限分配。 - [x] 部门管理:支持无限层级部门配置。
- [x] 角色管理:角色菜单权限,角色部门权限分配。
- [x] 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 - [x] 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
@ -171,10 +257,14 @@ git clone https://gitee.com/ktianc/kinit.git
### 准备工作 ### 准备工作
``` ```
Python == 3.10 (其他版本均未测试) 后端依赖版本:
nodejs >= 14.0 (推荐使用最新稳定版) Python == 3.10.x (其他版本均未测试)
Mysql >= 8.0 前端依赖版本:
MongoDB (推荐使用最新稳定版) nodejs >= 18.0 < 19
pnpm >= 8.1.0 < 9
数据库版本:
Mysql >= 8.0 (8 以上未测试以下版本未测试postgresql 未测试,更换可能会涉及调整)
MongoDB >= 7.0.12 < 8 (7 以上或以下版本均未测试)
Redis (推荐使用最新稳定版) Redis (推荐使用最新稳定版)
``` ```
@ -195,6 +285,7 @@ Redis (推荐使用最新稳定版)
```python ```python
# 安全警告: 不要在生产中打开调试运行! # 安全警告: 不要在生产中打开调试运行!
DEBUG = True # 如果当前为开发环境则改为 True如果为生产环境则改为 False DEBUG = True # 如果当前为开发环境则改为 True如果为生产环境则改为 False
```
3. 修改项目数据库配置信息 3. 修改项目数据库配置信息
@ -242,6 +333,7 @@ Redis (推荐使用最新稳定版)
# 文档https://user.ip138.com/ip/doc # 文档https://user.ip138.com/ip/doc
IP_PARSE_ENABLE = True IP_PARSE_ENABLE = True
IP_PARSE_TOKEN = "IP_PARSE_TOKEN" IP_PARSE_TOKEN = "IP_PARSE_TOKEN"
```
4. 并在`alembic.ini`文件中配置数据库信息,用于数据库映射 4. 并在`alembic.ini`文件中配置数据库信息,用于数据库映射
@ -251,13 +343,13 @@ Redis (推荐使用最新稳定版)
[dev] [dev]
# 开发环境 # 开发环境
version_locations = %(here)s/alembic/versions_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] [pro]
# 生产环境 # 生产环境
version_locations = %(here)s/alembic/versions_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. 创建数据库 5. 创建数据库
@ -413,7 +505,9 @@ pnpm run build:pro
DEBUG = False # 生产环境应该改为 False DEBUG = False # 生产环境应该改为 False
``` ```
3. 如果已有 Mysql 或者 Redis 或者 MongoDB 数据库,请修改如下内容,如果没有则不需要修改: 3. **如果没有安装数据库则不需要这一操作**)如果已有 Mysql 或者 Redis 或者 MongoDB 数据库,请执行以下操作:
请先在对应数据库中创建用户名以及数据库,并修改以下数据库连接改为已有的数据库连接
1. 修改 API 端配置文件: 1. 修改 API 端配置文件:
@ -422,7 +516,7 @@ pnpm run build:pro
```python ```python
# Mysql 数据库配置项 # Mysql 数据库配置项
# 连接引擎官方文档https://www.osgeo.cn/sqlalchemy/core/engines.html # 连接引擎官方文档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" SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@177.8.0.7:3306/kinit"
# Redis 数据库配置 # Redis 数据库配置
@ -438,9 +532,9 @@ pnpm run build:pro
``` ```
2. 修改定时任务配置文件 2. 修改定时任务配置文件
文件路径为:`kinit-task/application/config/production.py` 文件路径为:`kinit-task/application/config/production.py`
```python ```python
# Redis 数据库配置 # Redis 数据库配置
# 与接口是同一个数据库 # 与接口是同一个数据库
@ -457,7 +551,7 @@ pnpm run build:pro
``` ```
3. 将已有的数据库在 `docker-compose.yml` 文件中注释 3. 将已有的数据库在 `docker-compose.yml` 文件中注释
4. 配置阿里云 OSS 与 IP 解析接口地址(可选) 4. 配置阿里云 OSS 与 IP 解析接口地址(可选)
文件路径:`kinit-api/application/config/production.py` 文件路径:`kinit-api/application/config/production.py`
@ -484,7 +578,7 @@ pnpm run build:pro
IP_PARSE_ENABLE = False IP_PARSE_ENABLE = False
IP_PARSE_TOKEN = "IP_PARSE_TOKEN" IP_PARSE_TOKEN = "IP_PARSE_TOKEN"
``` ```
5. 前端项目打包: 5. 前端项目打包:
```shell ```shell
@ -622,4 +716,4 @@ docker-compose ps -a
<td><img src="https://k-typora.oss-cn-beijing.aliyuncs.com/kinit/1670077860987.jpg"/></td> <td><img src="https://k-typora.oss-cn-beijing.aliyuncs.com/kinit/1670077860987.jpg"/></td>
<td><img src="https://k-typora.oss-cn-beijing.aliyuncs.com/kinit/1670077870240.jpg"/></td> <td><img src="https://k-typora.oss-cn-beijing.aliyuncs.com/kinit/1670077870240.jpg"/></td>
</tr> </tr>
</table> </table>

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=/ VITE_BASE_PATH=/
@ -21,3 +21,15 @@ VITE_OUT_DIR=dist-dev
# 标题 # 标题
VITE_APP_TITLE=后台系统-开发 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=/ VITE_BASE_PATH=/
@ -21,3 +21,15 @@ VITE_OUT_DIR=dist-pro
# 标题 # 标题
VITE_APP_TITLE=后台系统 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/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 node_modules
.DS_Store .DS_Store
dist
dist-ssr dist-ssr
*.local *.local
*-lock.* *-lock.*
pnpm-debug pnpm-debug
stats.html
dist-pro
.vscode

View File

@ -3,7 +3,6 @@
/dist* /dist*
/public/* /public/*
/docs/* /docs/*
/vite.config.ts
/src/types/env.d.ts /src/types/env.d.ts
/docs/**/* /docs/**/*
/plop/**/* /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", "name": "vue-element-plus-admin",
"version": "2.2.0", "version": "2.7.0",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。", "description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>", "author": "Archer <502431556@qq.com>",
"private": false, "private": false,
"scripts": { "scripts": {
"i": "pnpm install", "i": "pnpm install",
"dev": "vite --mode dev", "dev": "pnpm vite --mode dev",
"ts:check": "vue-tsc --noEmit --skipLibCheck", "ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
"build:pro": "vite build --mode pro", "build:pro": "pnpm vite build --mode pro",
"build:dev": "vite build --mode dev", "build:dev": "pnpm vite build --mode dev",
"serve:pro": "vite preview --mode pro", "serve:pro": "pnpm vite preview --mode pro",
"serve:dev": "vite preview --mode dev", "serve:dev": "pnpm vite preview --mode dev",
"npm:check": "npx npm-check-updates", "npm:check": "pnpx npm-check-updates -u",
"clean": "npx rimraf node_modules", "clean": "pnpx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache", "clean:cache": "pnpx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", "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: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/", "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": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@iconify/iconify": "3.1.1",
"@iconify/iconify": "^3.1.1", "@iconify/vue": "4.1.1",
"@iconify/vue": "^4.1.1", "@vueuse/core": "10.9.0",
"@vueuse/core": "^10.3.0", "@wangeditor/editor": "5.1.23",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "5.1.10",
"@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/core": "^3.0.3", "animate.css": "4.1.1",
"animate.css": "^4.1.1", "axios": "1.6.7",
"axios": "^1.4.0", "cropperjs": "1.6.1",
"dayjs": "^1.11.9", "dayjs": "1.11.10",
"driver.js": "^1.2.1", "driver.js": "1.3.1",
"echarts": "^5.4.3", "echarts": "5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "2.1.0",
"element-plus": "^2.3.9", "element-plus": "2.5.6",
"lodash-es": "^4.17.21", "lodash-es": "4.17.21",
"mitt": "^3.0.1", "mitt": "3.0.1",
"mockjs": "^1.1.0", "nprogress": "0.2.0",
"nprogress": "^0.2.0", "pinia": "2.1.7",
"pinia": "^2.1.6", "pinia-plugin-persistedstate": "3.2.1",
"pinia-plugin-persist": "^1.0.0", "qrcode": "1.5.3",
"qrcode": "^1.5.3", "qs": "6.11.2",
"qs": "^6.11.2", "url": "0.11.3",
"sortablejs": "^1.15.0", "vue": "3.4.20",
"url": "^0.11.1", "vue-draggable-plus": "0.3.5",
"vue": "3.3.4", "vue-i18n": "9.9.1",
"vue-i18n": "9.2.2", "vue-json-pretty": "2.3.0",
"vue-json-pretty": "^2.2.4", "vue-router": "4.3.0",
"vue-router": "^4.2.4", "vue-types": "5.1.1",
"vue-types": "^5.1.1" "xgplayer": "3.0.13"
}, },
"devDependencies": { "devDependencies": {
"@iconify/json": "^2.2.101", "@amap/amap-jsapi-loader": "1.0.1",
"@intlify/unplugin-vue-i18n": "^0.12.2", "@commitlint/cli": "19.0.1",
"@kjgl77/datav-vue3": "^1.6.1", "@commitlint/config-conventional": "19.0.0",
"@purge-icons/generated": "^0.9.0", "@iconify/json": "2.2.187",
"@types/lodash-es": "^4.17.8", "@intlify/unplugin-vue-i18n": "2.0.0",
"@types/node": "^20.4.10", "@kjgl77/datav-vue3": "1.6.1",
"@types/nprogress": "^0.2.0", "@types/fs-extra": "11.0.4",
"@types/qrcode": "^1.5.1", "@types/inquirer": "9.0.7",
"@types/qs": "^6.9.7", "@types/lodash-es": "4.17.12",
"@types/sortablejs": "^1.15.1", "@types/node": "20.11.21",
"@typescript-eslint/eslint-plugin": "^6.3.0", "@types/nprogress": "0.2.3",
"@typescript-eslint/parser": "^6.3.0", "@types/qrcode": "1.5.5",
"@unocss/transformer-variant-group": "^0.55.0", "@types/qs": "6.9.12",
"@vitejs/plugin-legacy": "^4.1.1", "@types/sortablejs": "1.15.8",
"@vitejs/plugin-vue": "^4.2.3", "@typescript-eslint/eslint-plugin": "7.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.1", "@typescript-eslint/parser": "7.1.0",
"autoprefixer": "^10.4.14", "@unocss/transformer-variant-group": "0.58.5",
"consola": "^3.2.3", "@vitejs/plugin-legacy": "5.3.1",
"cron-validate": "^1.4.5", "@vitejs/plugin-vue": "5.0.4",
"eslint": "^8.47.0", "@vitejs/plugin-vue-jsx": "3.1.0",
"eslint-config-prettier": "^9.0.0", "autoprefixer": "10.4.17",
"eslint-define-config": "^1.23.0", "chalk": "5.3.0",
"eslint-plugin-prettier": "^5.0.0", "consola": "3.2.3",
"eslint-plugin-vue": "^9.17.0", "cron-validate": "1.4.5",
"intro.js": "^7.2.0", "eslint": "8.57.0",
"less": "^4.2.0", "eslint-config-prettier": "9.1.0",
"lint-staged": "^13.2.3", "eslint-define-config": "2.1.0",
"lodash": "^4.17.21", "eslint-plugin-prettier": "5.1.3",
"moment": "^2.29.4", "eslint-plugin-vue": "9.22.0",
"plop": "^3.1.2", "esno": "4.0.0",
"postcss": "^8.4.27", "fs-extra": "11.2.0",
"postcss-html": "^1.5.0", "inquirer": "9.2.15",
"postcss-less": "^6.0.0", "less": "4.2.0",
"prettier": "^3.0.1", "lint-staged": "15.2.2",
"rimraf": "^5.0.1", "lodash": "4.17.21",
"rollup": "^3.28.0", "moment": "2.30.1",
"stylelint": "^15.10.2", "plop": "4.0.1",
"stylelint-config-html": "^1.1.0", "postcss": "8.4.35",
"stylelint-config-recommended": "^13.0.0", "postcss-html": "1.6.0",
"stylelint-config-standard": "^34.0.0", "postcss-less": "6.0.0",
"stylelint-order": "^6.0.3", "prettier": "3.2.5",
"terser": "^5.19.2", "rimraf": "5.0.5",
"typescript": "5.1.6", "rollup": "4.12.0",
"unocss": "^0.55.0", "rollup-plugin-visualizer": "5.12.0",
"vite": "4.4.9", "stylelint": "16.2.1",
"vite-plugin-ejs": "^1.6.4", "stylelint-config-html": "1.1.0",
"vite-plugin-eslint": "^1.8.1", "stylelint-config-recommended": "14.0.0",
"vite-plugin-mock": "2.9.6", "stylelint-config-standard": "36.0.0",
"vite-plugin-progress": "^0.0.7", "stylelint-order": "6.0.4",
"vite-plugin-purge-icons": "^0.9.2", "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-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vue-draggable-plus": "^0.2.6", "vue-draggable-plus": "0.2.6",
"vue-tsc": "^1.8.8", "vite-plugin-url-copy": "1.1.3",
"vue3-json-viewer": "^2.2.2" "vue-tsc": "1.8.27",
"vue3-json-viewer": "2.2.2",
"zipson": "0.2.12"
}, },
"packageManager": "pnpm@8.1.0",
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">=18 <19",
"pnpm": ">=8.1.0 <10.0.0"
}, },
"license": "MIT", "license": "MIT",
"repository": { "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 { computed } from 'vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { ConfigGlobal } from '@/components/ConfigGlobal' import { ConfigGlobal } from '@/components/ConfigGlobal'
import { isDark } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useStorage } from '@/hooks/web/useStorage'
import { getSystemBaseConfigApi } from '@/api/vadmin/system/settings' import { getSystemBaseConfigApi } from '@/api/vadmin/system/settings'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -17,18 +15,6 @@ const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode) 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 // mate
const addMeta = (name: string, content: string) => { const addMeta = (name: string, content: string) => {
const meta = document.createElement('meta') const meta = document.createElement('meta')
@ -39,6 +25,9 @@ const addMeta = (name: string, content: string) => {
// //
const setSystemConfig = async () => { const setSystemConfig = async () => {
if (appStore.getLogoImage) {
return
}
const res = await getSystemBaseConfigApi() const res = await getSystemBaseConfigApi()
if (res) { if (res) {
appStore.setTitle(res.data.web_title || import.meta.env.VITE_APP_TITLE) appStore.setTitle(res.data.web_title || import.meta.env.VITE_APP_TITLE)
@ -52,7 +41,7 @@ const setSystemConfig = async () => {
} }
} }
setDefaultTheme() appStore.initTheme()
setSystemConfig() setSystemConfig()
</script> </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' import request from '@/config/axios'
export const getSystemSettingsTabsApi = (params: any): Promise<IResponse> => { export const getSystemSettingsTabsApi = (data: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/tabs', params }) return request.post({ url: '/vadmin/system/settings/tabs', data })
} }
export const getSystemSettingsApi = (params: any): Promise<IResponse> => { 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"> <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 { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { ref, unref, PropType, computed, defineComponent } from 'vue' import { ref, unref, PropType, computed, defineComponent } from 'vue'
@ -16,6 +16,8 @@ const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('descriptions') const prefixCls = getPrefixCls('descriptions')
const defaultData = '-'
export default defineComponent({ export default defineComponent({
name: 'Descriptions', name: 'Descriptions',
props: { props: {
@ -36,7 +38,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
setup(props, { slots, attrs }) { setup(props, { attrs }) {
const getBindValue = computed((): any => { const getBindValue = computed((): any => {
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class'] const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
const obj = { ...attrs, ...props } const obj = { ...attrs, ...props }
@ -59,7 +61,10 @@ export default defineComponent({
delete obj[key] delete obj[key]
} }
} }
return obj return {
labelClassName: `${prefixCls}-label`,
...obj
}
} }
// //
@ -103,26 +108,51 @@ export default defineComponent({
<ElCollapseTransition> <ElCollapseTransition>
<div v-show={unref(show)} class={[`${prefixCls}-content`, 'p-20px']}> <div v-show={unref(show)} class={[`${prefixCls}-content`, 'p-20px']}>
<ElDescriptions {...unref(getBindValue)}> <ElRow
{{ gutter={0}
extra: () => (slots['extra'] ? slots['extra']() : props.extra), {...unref(getBindValue)}
default: () => { class="outline-1px outline-[var(--el-border-color-lighter)] outline-solid"
return props.schema.map((item) => { >
return ( {props.schema.map((item) => {
<ElDescriptionsItem key={item.field} {...getBindItemValue(item)}> return (
{{ <ElCol
label: () => (item.slots?.label ? item.slots?.label(item) : item.label), key={item.field}
default: () => span={item.span || 24 / props.column}
item.slots?.default class="flex items-stretch"
? item.slots?.default(props.data) >
: get(props.data, item.field) {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">
</ElDescriptionsItem> <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}
</ElDescriptions> </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> </div>
</ElCollapseTransition> </ElCollapseTransition>
</div> </div>
@ -153,9 +183,13 @@ export default defineComponent({
} }
} }
.@{prefix-cls}-content { :deep(.@{prefix-cls}-label) {
:deep(.@{elNamespace}-descriptions__cell) { width: 150px !important;
width: 0;
}
} }
// .@{prefix-cls}-content {
// :deep(.@{elNamespace}-descriptions__cell) {
// width: 0;
// }
// }
</style> </style>

View File

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

View File

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

View File

@ -9,14 +9,14 @@ const prefixCls = getPrefixCls('footer')
const appStore = useAppStore() const appStore = useAppStore()
const title = computed(() => appStore.getTitle) const footerContent = computed(() => appStore.getFooterContent)
</script> </script>
<template> <template>
<div <div
:class="prefixCls" :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> </div>
</template> </template>

View File

@ -95,9 +95,6 @@ export default defineComponent({
// element form // element form
const elFormRef = ref<ComponentRef<typeof ElForm>>() const elFormRef = ref<ComponentRef<typeof ElForm>>()
// useFormprops
const outsideProps = ref<FormProps>({})
const mergeProps = ref<FormProps>({}) const mergeProps = ref<FormProps>({})
const getProps = computed(() => { const getProps = computed(() => {
@ -155,8 +152,6 @@ export default defineComponent({
const setProps = (props: FormProps = {}) => { const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props) mergeProps.value = Object.assign(unref(mergeProps), props)
// @ts-ignore
outsideProps.value = props
} }
const delSchema = (field: string) => { 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 <Com
vModel={itemVal.value} vModel={itemVal.value}
ref={(el: any) => setComponentRefMap(el, item.field)} ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))} {...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)} {...setComponentProps(item)}
style={item.componentProps?.style || {}} style={
item.componentProps?.style || {
width: '100%'
}
}
> >
{{ ...slotsMap }} {{ ...slotsMap }}
</Com> </Com>
@ -447,6 +460,10 @@ export default defineComponent({
{...getFormBindValue()} {...getFormBindValue()}
model={unref(getProps).isCustom ? unref(getProps).model : formModel} model={unref(getProps).isCustom ? unref(getProps).model : formModel}
class={prefixCls} class={prefixCls}
// @ts-ignore
onSubmit={(e: Event) => {
e.preventDefault()
}}
> >
{{ {{
// //
@ -466,4 +483,16 @@ export default defineComponent({
margin-right: 0 !important; margin-right: 0 !important;
margin-left: 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> </style>

View File

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

View File

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

View File

@ -25,6 +25,11 @@ const symbolId = computed(() => {
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon 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 getIconifyStyle = computed(() => {
const { color, size } = props const { color, size } = props
return { return {
@ -40,7 +45,10 @@ const getIconifyStyle = computed(() => {
<use :xlink:href="symbolId" /> <use :xlink:href="symbolId" />
</svg> </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> </ElIcon>
</template> </template>
@ -49,11 +57,18 @@ const getIconifyStyle = computed(() => {
.@{prefix-cls}, .@{prefix-cls},
.iconify { .iconify {
&:hover { :deep(svg) {
:deep(svg) { &:hover {
// stylelint-disable-next-line // stylelint-disable-next-line
color: v-bind(hoverColor) !important; color: v-bind(hoverColor) !important;
} }
} }
} }
.iconify {
&:hover {
// stylelint-disable-next-line
color: v-bind(hoverColor) !important;
}
}
</style> </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; background-color: transparent;
border-color: var(--el-color-white); border-color: var(--el-color-white);
border-style: solid; border-style: solid;
border-width: 0 5px 0 5px; border-width: 0 5px;
content: ''; content: '';
} }

View File

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

View File

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

View File

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

View File

@ -10,10 +10,14 @@ export const useRenderMenuTitle = () => {
return icon ? ( return icon ? (
<> <>
<Icon icon={meta.icon}></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} { .@{prefix-cls} {
&--disabled { &--disabled {
background: rgba(255, 255, 255, 0.95); background: rgb(255 255 255 / 95%);
& > div { & > div {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,9 @@ import {
ElPagination, ElPagination,
ComponentSize, ComponentSize,
ElTooltipProps, ElTooltipProps,
ElImage ElImage,
ElEmpty,
ElCard
} from 'element-plus' } from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue' import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
@ -15,9 +17,10 @@ import { set, get } from 'lodash-es'
import { CSSProperties } from 'vue' import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper' import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue' import TableActions from './components/TableActions.vue'
// import Sortable from 'sortablejs'
// import { Icon } from '@/components/Icon'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { createVideoViewer } from '@/components/VideoPlayer'
import { Icon } from '@/components/Icon'
import { BaseButton } from '@/components/Button'
const appStore = useAppStore() const appStore = useAppStore()
@ -60,8 +63,13 @@ export default defineComponent({
type: Array as PropType<Recordable[]>, type: Array as PropType<Recordable[]>,
default: () => [] default: () => []
}, },
// //
preview: { imagePreview: {
type: Array as PropType<string[]>,
default: () => []
},
//
videoPreview: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
default: () => [] default: () => []
}, },
@ -72,7 +80,7 @@ export default defineComponent({
border: propTypes.bool.def(true), border: propTypes.bool.def(true),
size: { size: {
type: String as PropType<ComponentSize>, 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), fit: propTypes.bool.def(true),
showHeader: propTypes.bool.def(true), showHeader: propTypes.bool.def(true),
@ -191,9 +199,27 @@ export default defineComponent({
default: 'fixed' default: 'fixed'
}, },
scrollbarAlwaysOn: propTypes.bool.def(false), 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 }) { setup(props, { attrs, emit, slots, expose }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>() const elTableRef = ref<ComponentRef<typeof ElTable>>()
@ -218,33 +244,6 @@ export default defineComponent({
return propsObj 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 = {}) => { const setProps = (props: TableProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props) mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = { ...props } as any outsideProps.value = { ...props } as any
@ -265,7 +264,7 @@ export default defineComponent({
const addColumn = (column: TableColumn, index?: number) => { const addColumn = (column: TableColumn, index?: number) => {
const { columns } = unref(getProps) const { columns } = unref(getProps)
if (index) { if (index !== void 0) {
columns.splice(index, 0, column) columns.splice(index, 0, column)
} else { } else {
columns.push(column) columns.push(column)
@ -350,11 +349,13 @@ export default defineComponent({
const bindValue: Recordable = { ...attrs, ...unref(getProps) } const bindValue: Recordable = { ...attrs, ...unref(getProps) }
delete bindValue.columns delete bindValue.columns
delete bindValue.data delete bindValue.data
delete bindValue.align
return bindValue return bindValue
}) })
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => { const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps) const { align, headerAlign, showOverflowTooltip, imagePreview, videoPreview } =
unref(getProps)
return columnsChildren.map((v) => { return columnsChildren.map((v) => {
if (v.show === false) return null if (v.show === false) return null
const props = { ...v } as any const props = { ...v } as any
@ -365,20 +366,20 @@ export default defineComponent({
const slots = { const slots = {
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false let isPreview = false
if (preview.length) { isPreview =
isImageUrl = preview.some((item) => (item as string) === v.field) imagePreview.some((item) => (item as string) === v.field) ||
} videoPreview.some((item) => (item as string) === v.field)
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(...args) ? props.slots.default(...args)
: v?.formatter : v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index) ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl : isPreview
? renderPreview(get(data.row, v.field)) ? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field) : get(data.row, v.field)
} }
} }
if (props?.slots?.header) { 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 ( return (
<div class="flex items-center"> <div class="flex items-center">
<ElImage {imagePreview.includes(field) ? (
src={url} <ElImage
fit="cover" src={url}
class="w-[100%] h-100px" fit="cover"
lazy class="w-[100%]"
preview-src-list={[url]} lazy
preview-teleported preview-src-list={[url]}
/> preview-teleported
/>
) : videoPreview.includes(field) ? (
<BaseButton
type="primary"
icon={<Icon icon="ep:video-play" />}
onClick={() => {
createVideoViewer({
url
})
}}
>
预览
</BaseButton>
) : null}
</div> </div>
) )
} }
@ -424,7 +440,8 @@ export default defineComponent({
headerAlign, headerAlign,
showOverflowTooltip, showOverflowTooltip,
reserveSelection, reserveSelection,
preview imagePreview,
videoPreview
} = unref(getProps) } = unref(getProps)
return (columnsChildren || columns).map((v) => { return (columnsChildren || columns).map((v) => {
@ -450,6 +467,7 @@ export default defineComponent({
reserveSelection={reserveSelection} reserveSelection={reserveSelection}
align="center" align="center"
headerAlign="center" headerAlign="center"
selectable={v.selectable}
width="50px" width="50px"
fixed="left" fixed="left"
></ElTableColumn> ></ElTableColumn>
@ -464,20 +482,20 @@ export default defineComponent({
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false let isPreview = false
if (preview.length) { isPreview =
isImageUrl = preview.some((item) => (item as string) === v.field) imagePreview.some((item) => (item as string) === v.field) ||
} videoPreview.some((item) => (item as string) === v.field)
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(...args) ? props.slots.default(...args)
: v?.formatter : v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index) ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl : isPreview
? renderPreview(get(data.row, v.field)) ? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field) : get(data.row, v.field)
} }
} }
if (props?.slots?.header) { if (props?.slots?.header) {
@ -509,52 +527,79 @@ export default defineComponent({
} }
const toolbar = getSlot(slots, 'toolbar') 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 ( return (
<div v-loading={unref(getProps).loading}> <div v-loading={unref(getProps).loading}>
<div class="flex justify-between mb-1"> {unref(getProps).customContent ? (
<div>{toolbar}</div> <div class="flex flex-wrap">
<div class="pt-2"> {unref(getProps)?.data?.length ? (
{unref(getProps).showAction ? ( unref(getProps)?.data.map((item) => {
<TableActions const cardSlots = {
activeUID={unref(getProps).activeUID} default: () => {
columns={unref(getProps).columns} return getSlot(slots, 'content', item)
el-table-ref={elTableRef} }
onChangSize={changSize} }
onRefresh={refresh} if (getSlot(slots, 'content-header')) {
/> cardSlots['header'] = () => {
) : null} 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> ) : (
<>
<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 ? ( {unref(getProps).pagination ? (
<ElPagination <ElPagination
v-model:pageSize={pageSizeRef.value} v-model:pageSize={pageSizeRef.value}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus' import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n' 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 { useDesign } from '@/hooks/web/useDesign'
import LockDialog from './components/LockDialog.vue' import LockDialog from './components/LockDialog.vue'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@ -14,7 +14,7 @@ const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false) const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const authStore = useAuthStoreWithOut() const authStore = useAuthStore()
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -73,13 +73,13 @@ const user = computed(() => authStore.getUser)
<template #dropdown> <template #dropdown>
<ElDropdownMenu> <ElDropdownMenu>
<ElDropdownItem> <ElDropdownItem>
<ElButton @click="toHome" link>个人主页</ElButton> <BaseButton @click="toHome" link>个人主页</BaseButton>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem> <ElDropdownItem>
<ElButton @click="toGitee" link>Gitee</ElButton> <BaseButton @click="toGitee" link>Gitee</BaseButton>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem> <ElDropdownItem>
<ElButton @click="toGithub" link>Github</ElButton> <BaseButton @click="toGithub" link>Github</BaseButton>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem divided> <ElDropdownItem divided>
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div> <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 { reactive, computed } from 'vue'
import { useValidator } from '@/hooks/web/useValidator' import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/components/Form' import { FormSchema } from '@/components/Form'
import { ElButton } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock' import { useLockStore } from '@/store/modules/lock'
@ -87,14 +86,14 @@ const handleLock = async () => {
</div> </div>
<Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" /> <Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
<template #footer> <template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton> <BaseButton type="primary" @click="handleLock">{{ t('lock.lock') }}</BaseButton>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
:global(.v-lock-dialog) { :global(.v-lock-dialog) {
@media (max-width: 767px) { @media (width <= 767px) {
max-width: calc(100vw - 16px); max-width: calc(100vw - 16px);
} }
} }

View File

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

View File

@ -1,7 +1,5 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios' import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { useStorage } from '@/hooks/web/useStorage' import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useAppStore } from '@/store/modules/app'
import { useAuthStore } from '@/store/modules/auth'
import qs from 'qs' import qs from 'qs'
import { config } from './config' import { config } from './config'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@ -9,8 +7,6 @@ import request from '@/config/axios'
const { result_code, unauthorized_code, request_timeout } = config const { result_code, unauthorized_code, request_timeout } = config
const { getStorage, setStorage } = useStorage()
// 创建axios实例 // 创建axios实例
const service: AxiosInstance = axios.create({ const service: AxiosInstance = axios.create({
baseURL: '/api', // api 的 base_url baseURL: '/api', // api 的 base_url
@ -21,10 +17,10 @@ const service: AxiosInstance = axios.create({
// request拦截器 // request拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const appStore = useAppStore() const authStore = useAuthStoreWithOut()
const token = getStorage(appStore.getToken) const token = authStore.getToken
if (token !== '') { if (token !== '') {
;(config.headers as any)['Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改 ;(config.headers as any)[authStore.getTokenKey ?? 'Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
} }
if ( if (
config.method === 'post' && config.method === 'post' &&
@ -87,18 +83,18 @@ service.interceptors.response.use(
if (refresh === '1') { if (refresh === '1') {
// 因token快过期刷新token // 因token快过期刷新token
refreshToken().then((res) => { refreshToken().then((res) => {
const appStore = useAppStore() const authStore = useAuthStoreWithOut()
setStorage(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`) authStore.setToken(`${res.data.token_type} ${res.data.access_token}`)
setStorage(appStore.getRefreshToken, res.data.refresh_token) authStore.setRefreshToken(res.data.refresh_token)
}) })
} }
return response.data return response.data
} else if (code === unauthorized_code) { } else if (code === unauthorized_code) {
// 因token无效token过期导致 // 因token无效token过期导致
refreshToken().then((res) => { refreshToken().then((res) => {
const appStore = useAppStore() const authStore = useAuthStoreWithOut()
setStorage(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`) authStore.setToken(`${res.data.token_type} ${res.data.access_token}`)
setStorage(appStore.getRefreshToken, res.data.refresh_token) authStore.setRefreshToken(res.data.refresh_token)
ElMessage.error('操作失败,请重试') ElMessage.error('操作失败,请重试')
}) })
} else { } else {
@ -108,7 +104,7 @@ service.interceptors.response.use(
(error: AxiosError) => { (error: AxiosError) => {
console.log('err', error) console.log('err', error)
let { message } = error let { message } = error
const authStore = useAuthStore() const authStore = useAuthStoreWithOut()
const status = error.response?.status const status = error.response?.status
switch (status) { switch (status) {
case 400: case 400:
@ -117,7 +113,7 @@ service.interceptors.response.use(
case 401: case 401:
// 强制要求重新登录因账号已冻结账号已过期手机号码错误刷新token无效等问题导致 // 强制要求重新登录因账号已冻结账号已过期手机号码错误刷新token无效等问题导致
authStore.logout() authStore.logout()
message = '认证已过期,请重新登录' message = '认证已失效,请重新登录'
break break
case 403: case 403:
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致 // 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
@ -158,8 +154,8 @@ service.interceptors.response.use(
// 刷新Token // 刷新Token
const refreshToken = (): Promise<IResponse> => { const refreshToken = (): Promise<IResponse> => {
const appStore = useAppStore() const authStore = useAuthStoreWithOut()
const data = getStorage(appStore.getRefreshToken) const data = authStore.getRefreshToken
return request.post({ url: '/auth/token/refresh', data }) 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' import { useAuthStoreWithOut } from '@/store/modules/auth'
const { t } = useI18n() const { t } = useI18n()
const authStore = useAuthStoreWithOut()
// 全部权限 // 全部权限
const all_permission = ['*.*.*'] const all_permission = ['*.*.*']
const hasPermission = (value: string | string[]): boolean => { const hasPermission = (value: string | string[]): boolean => {
const authStore = useAuthStoreWithOut()
const permissions = authStore.getPermissions const permissions = authStore.getPermissions
if (!value) { if (!value) {
throw new Error(t('permission.hasPermission')) 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++) { for (let i = 0; i < length; i++) {
const schemaItem = crudSchema[i] const schemaItem = crudSchema[i]
// 判断是否隐藏 // 判断是否隐藏
if (!schemaItem?.search?.hidden) { const searchSchemaItem = {
const searchSchemaItem = { component: schemaItem?.search?.component || 'Input',
component: schemaItem?.search?.component || 'Input', ...schemaItem.search,
...schemaItem.search, field: schemaItem.field,
field: schemaItem.field, label: schemaItem.search?.label || schemaItem.label
label: schemaItem.label
}
// 删除不必要的字段
delete searchSchemaItem.hidden
searchSchema.push(searchSchemaItem)
} }
searchSchema.push(searchSchemaItem)
} }
return searchSchema return searchSchema
@ -103,8 +98,8 @@ const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
conversion: (schema: CrudSchema) => { conversion: (schema: CrudSchema) => {
if (!schema?.table?.hidden) { if (!schema?.table?.hidden) {
return { return {
...schema.table, ...schema,
...schema ...schema.table
} }
} }
} }
@ -127,19 +122,14 @@ const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const formItem = crudSchema[i] const formItem = crudSchema[i]
// 判断是否隐藏 // 判断是否隐藏
if (!formItem?.form?.hidden) { const formSchemaItem = {
const formSchemaItem = { component: formItem?.form?.component || 'Input',
component: formItem?.form?.component || 'Input', ...formItem.form,
...formItem.form, field: formItem.field,
field: formItem.field, label: formItem.form?.label || formItem.label
label: formItem.label
}
// 删除不必要的字段
delete formSchemaItem.hidden
formSchema.push(formSchemaItem)
} }
formSchema.push(formSchemaItem)
} }
return formSchema return formSchema

View File

@ -2,6 +2,7 @@ import type { Form, FormExpose } from '@/components/Form'
import type { ElForm, ElFormItem } from 'element-plus' import type { ElForm, ElFormItem } from 'element-plus'
import { ref, unref, nextTick } from 'vue' import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps, FormProps } from '@/components/Form' import { FormSchema, FormSetProps, FormProps } from '@/components/Form'
import { isEmptyVal, isObject } from '@/utils/is'
export const useForm = () => { export const useForm = () => {
// From实例 // From实例
@ -93,9 +94,27 @@ export const useForm = () => {
* @description * @description
* @returns form data * @returns form data
*/ */
getFormData: async <T = Recordable>(): Promise<T> => { getFormData: async <T = Recordable>(filterEmptyVal = true): Promise<T> => {
const form = await getForm() 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' import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
export const usePageLoading = () => { export const usePageLoading = () => {
const loadStart = () => { const loadStart = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(true) appStore.setPageLoading(true)
} }
const loadDone = () => { const loadDone = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(false) appStore.setPageLoading(false)
} }

View File

@ -4,7 +4,7 @@ const getValueType = (value: any) => {
return type.slice(8, -1) 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 setStorage = (key: string, value: any) => {
const valueType = getValueType(value) const valueType = getValueType(value)
window[type].setItem(key, JSON.stringify({ type: valueType, 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 { useAppStoreWithOut } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
const appStore = useAppStoreWithOut()
export const useTitle = (newTitle?: string) => { export const useTitle = (newTitle?: string) => {
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStoreWithOut()
const title = ref( const title = ref(
newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
) )

View File

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

View File

@ -6,10 +6,6 @@ import { computed } from 'vue'
const appStore = useAppStore() const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const fixedHeader = computed(() => appStore.getFixedHeader)
const footer = computed(() => appStore.getFooter) const footer = computed(() => appStore.getFooter)
const tagsViewStore = useTagsViewStore() const tagsViewStore = useTagsViewStore()
@ -17,39 +13,12 @@ const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => { const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews return tagsViewStore.getCachedViews
}) })
const tagsView = computed(() => appStore.getTagsView)
</script> </script>
<template> <template>
<section <section
:class="[ :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)]', '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)]'
{
'!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
}
]" ]"
> >
<router-view> <router-view>

View File

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

View File

@ -195,8 +195,8 @@ export const useRenderLayout = () => {
`${prefixCls}-content`, `${prefixCls}-content`,
'w-full', 'w-full',
{ {
'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value, 'h-[calc(100%-var(--top-tool-height))]': !fixedHeader.value,
'h-[calc(100%-var(--tags-view-height)-var(--app-footer-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', notSpace: 'Spaces are not allowed',
notSpecialCharacters: 'Special characters are not allowed', notSpecialCharacters: 'Special characters are not allowed',
isEqual: 'The two are not equal', isEqual: 'The two are not equal',
// 列设置
setting: 'Setting',
selectAll: 'Select all', selectAll: 'Select all',
SerialNumberColumn: 'Index column' SerialNumberColumn: 'Index column'
}, },
@ -186,7 +188,15 @@ export default {
permission: 'Permission test page', permission: 'Permission test page',
function: 'Function', function: 'Function',
multipleTabs: 'Multiple tabs', 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: { permission: {
hasPermission: 'Please set the operation permission value' hasPermission: 'Please set the operation permission value'
@ -332,7 +342,8 @@ export default {
lazyLoad: 'Lazy load', lazyLoad: 'Lazy load',
upload: 'Upload', upload: 'Upload',
// 用户头像 // 用户头像
userAvatar: 'User avatar' userAvatar: 'User avatar',
iconPicker: 'Icon picker'
}, },
guideDemo: { guideDemo: {
guide: 'Guide', guide: 'Guide',
@ -461,7 +472,9 @@ export default {
fixedHeaderOrAuto: 'Fixed header or auto', fixedHeaderOrAuto: 'Fixed header or auto',
getSelections: 'Get selections', getSelections: 'Get selections',
preview: 'Preview', preview: 'Preview',
showOrHiddenSortable: 'Show or hidden sortable' showOrHiddenSortable: 'Show or hidden sortable',
videoPreview: 'Video preview',
cardTable: 'Card table'
}, },
richText: { richText: {
richText: 'Rich text', richText: 'Rich text',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,6 +70,37 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
noTagsView: true 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', path: '/404',
component: () => import('@/views/Error/404.vue'), component: () => import('@/views/Error/404.vue'),
@ -115,7 +146,17 @@ const router = createRouter({
}) })
export const resetRouter = (): void => { export const resetRouter = (): void => {
const resetWhiteNameList = ['Login', 'NoFind', 'Root'] const resetWhiteNameList = [
'Login',
'NoFind',
'Root',
'ResetPassword',
'Redirect',
'Home',
'Docs',
'Privacy',
'Agreement'
]
router.getRoutes().forEach((route) => { router.getRoutes().forEach((route) => {
const { name } = route const { name } = route
if (name && !resetWhiteNameList.includes(name as string)) { 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>) => { export const setupRouter = (app: App<Element>) => {
app.use(router) app.use(router)
} }

View File

@ -1,10 +1,12 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createPinia } from 'pinia' 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() const store = createPinia()
store.use(piniaPersist) store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => { export const setupStore = (app: App<Element>) => {
app.use(store) app.use(store)

View File

@ -2,9 +2,10 @@ import { defineStore } from 'pinia'
import { store } from '../index' import { store } from '../index'
import { setCssVar, humpToUnderline } from '@/utils' import { setCssVar, humpToUnderline } from '@/utils'
import { ElMessage, ComponentSize } from 'element-plus' import { ElMessage, ComponentSize } from 'element-plus'
import { useStorage } from '@/hooks/web/useStorage' import { colorIsDark, hexToRGB, lighten, mix } from '@/utils/color'
import { useCssVar } from '@vueuse/core'
const { getStorage, setStorage } = useStorage() import { unref } from 'vue'
import { useDark } from '@vueuse/core'
interface AppState { interface AppState {
breadcrumb: boolean breadcrumb: boolean
@ -25,7 +26,6 @@ interface AppState {
pageLoading: boolean pageLoading: boolean
layout: LayoutType layout: LayoutType
title: string title: string
userInfo: string
isDark: boolean isDark: boolean
currentSize: ComponentSize currentSize: ComponentSize
sizeMap: ComponentSize[] sizeMap: ComponentSize[]
@ -34,8 +34,6 @@ interface AppState {
theme: ThemeTypes theme: ThemeTypes
fixedMenu: boolean fixedMenu: boolean
token: string
refreshToken: string
logoImage: string logoImage: string
footerContent: string footerContent: string
icpNumber: string icpNumber: string
@ -44,7 +42,6 @@ interface AppState {
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: (): AppState => { state: (): AppState => {
return { return {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其它项目冲突
sizeMap: ['default', 'large', 'small'], sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端 mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题 title: import.meta.env.VITE_APP_TITLE, // 标题
@ -63,14 +60,14 @@ export const useAppStore = defineStore('app', {
fixedHeader: true, // 固定toolheader fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚 footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日 greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: getStorage('dynamicRouter'), // 是否动态路由 dynamicRouter: true, // 是否动态路由
serverDynamicRouter: getStorage('serverDynamicRouter'), // 是否服务端渲染动态路由 serverDynamicRouter: true, // 是否服务端渲染动态路由
fixedMenu: getStorage('fixedMenu'), // 是否固定菜单 fixedMenu: false, // 是否固定菜单
layout: getStorage('layout') || 'classic', // layout布局 layout: 'classic', // layout布局
isDark: getStorage('isDark'), // 是否是暗黑模式 isDark: false, // 是否是暗黑模式
currentSize: getStorage('default') || 'default', // 组件尺寸 currentSize: 'default', // 组件尺寸
theme: getStorage('theme') || { theme: {
// 主题色 // 主题色
elColorPrimary: '#409eff', elColorPrimary: '#409eff',
// 左侧菜单边框颜色 // 左侧菜单边框颜色
@ -101,8 +98,6 @@ export const useAppStore = defineStore('app', {
topToolBorderColor: '#eee' topToolBorderColor: '#eee'
}, },
token: 'Token', // 存储Token字段
refreshToken: 'RefreshToken', // 存储刷新Token字段
logoImage: '', // logo图片 logoImage: '', // logo图片
footerContent: '', // 页脚内容 footerContent: '', // 页脚内容
icpNumber: '' // 备案号 icpNumber: '' // 备案号
@ -166,9 +161,6 @@ export const useAppStore = defineStore('app', {
getTitle(): string { getTitle(): string {
return this.title return this.title
}, },
getUserInfo(): string {
return this.userInfo
},
getIsDark(): boolean { getIsDark(): boolean {
return this.isDark return this.isDark
}, },
@ -191,12 +183,6 @@ export const useAppStore = defineStore('app', {
getLogoImage(): string { getLogoImage(): string {
return this.logoImage return this.logoImage
}, },
getToken(): string {
return this.token
},
getRefreshToken(): string {
return this.refreshToken
},
getFooterContent(): string { getFooterContent(): string {
return this.footerContent return this.footerContent
}, },
@ -245,15 +231,12 @@ export const useAppStore = defineStore('app', {
this.greyMode = greyMode this.greyMode = greyMode
}, },
setDynamicRouter(dynamicRouter: boolean) { setDynamicRouter(dynamicRouter: boolean) {
setStorage('dynamicRouter', dynamicRouter)
this.dynamicRouter = dynamicRouter this.dynamicRouter = dynamicRouter
}, },
setServerDynamicRouter(serverDynamicRouter: boolean) { setServerDynamicRouter(serverDynamicRouter: boolean) {
setStorage('serverDynamicRouter', serverDynamicRouter)
this.serverDynamicRouter = serverDynamicRouter this.serverDynamicRouter = serverDynamicRouter
}, },
setFixedMenu(fixedMenu: boolean) { setFixedMenu(fixedMenu: boolean) {
setStorage('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu this.fixedMenu = fixedMenu
}, },
setPageLoading(pageLoading: boolean) { setPageLoading(pageLoading: boolean) {
@ -265,7 +248,6 @@ export const useAppStore = defineStore('app', {
return return
} }
this.layout = layout this.layout = layout
setStorage('layout', this.layout)
}, },
setTitle(title: string) { setTitle(title: string) {
this.title = title this.title = title
@ -279,23 +261,22 @@ export const useAppStore = defineStore('app', {
document.documentElement.classList.add('light') document.documentElement.classList.add('light')
document.documentElement.classList.remove('dark') document.documentElement.classList.remove('dark')
} }
setStorage('isDark', this.isDark) this.setPrimaryLight()
}, },
setCurrentSize(currentSize: ComponentSize) { setCurrentSize(currentSize: ComponentSize) {
this.currentSize = currentSize this.currentSize = currentSize
setStorage('currentSize', this.currentSize)
}, },
setMobile(mobile: boolean) { setMobile(mobile: boolean) {
this.mobile = mobile this.mobile = mobile
}, },
setTheme(theme: ThemeTypes) { setTheme(theme: ThemeTypes) {
this.theme = Object.assign(this.theme, theme) this.theme = Object.assign(this.theme, theme)
setStorage('theme', this.theme)
}, },
setCssVarTheme() { setCssVarTheme() {
for (const key in this.theme) { for (const key in this.theme) {
setCssVar(`--${humpToUnderline(key)}`, this.theme[key]) setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
} }
this.setPrimaryLight()
}, },
setFooter(footer: boolean) { setFooter(footer: boolean) {
this.footer = footer this.footer = footer
@ -309,8 +290,75 @@ export const useAppStore = defineStore('app', {
}, },
setIcpNumber(icpNumber: string) { setIcpNumber(icpNumber: string) {
this.icpNumber = icpNumber 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 = () => { export const useAppStoreWithOut = () => {

View File

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

View File

@ -10,7 +10,11 @@ export const useDictStore = defineStore('dict', {
state: (): DictState => ({ state: (): DictState => ({
dictObj: {} dictObj: {}
}), }),
getters: {}, getters: {
getDictObjData(): Recordable {
return this.dictObj
}
},
actions: { actions: {
async getDictObj(dictTypes: string[]) { async getDictObj(dictTypes: string[]) {
const result: Recordable = {} const result: Recordable = {}
@ -34,7 +38,8 @@ export const useDictStore = defineStore('dict', {
} }
return result return result
} }
} },
persist: true
}) })
export const useDictStoreWithOut = () => { 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 { useStorage } from '@/hooks/web/useStorage'
import { LocaleDropdownType } from '@/components/LocaleDropdown' import { LocaleDropdownType } from '@/components/LocaleDropdown'
const { getStorage, setStorage } = useStorage() const { getStorage, setStorage } = useStorage('localStorage')
const elLocaleMap = { const elLocaleMap = {
'zh-CN': zhCn, 'zh-CN': zhCn,
@ -51,7 +51,8 @@ export const useLocaleStore = defineStore('locales', {
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
setStorage('lang', localeMap?.lang) setStorage('lang', localeMap?.lang)
} }
} },
persist: true
}) })
export const useLocaleStoreWithOut = () => { export const useLocaleStoreWithOut = () => {

View File

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

View File

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

View File

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

View File

@ -151,3 +151,22 @@ const subtractLight = (color: string, amount: number) => {
const c = cc < 0 ? 0 : cc const c = cc < 0 ? 0 : cc
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` 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 * @param component
@ -47,6 +45,10 @@ export const setCssVar = (prop: string, val: any, dom = document.documentElement
dom.style.setProperty(prop, val) dom.style.setProperty(prop, val)
} }
export const getCssVar = (prop: string, dom = document.documentElement) => {
return getComputedStyle(dom).getPropertyValue(prop)
}
/** /**
* *
* @param {Array} ary * @param {Array} ary
@ -123,6 +125,17 @@ export function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) 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 => { export const getGreeting = (): string => {
const now = new Date() 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' import { CSSProperties } from 'vue'
// 自定义扩展vue-types
type PropTypes = VueTypesInterface & { type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties> readonly style: VueTypeValidableDef<CSSProperties>
} }
const propTypes = createTypes({ const newPropTypes = createTypes({
func: undefined, func: undefined,
bool: undefined, bool: undefined,
string: undefined, string: undefined,
@ -15,15 +14,12 @@ const propTypes = createTypes({
integer: undefined integer: undefined
}) as PropTypes }) as PropTypes
// 需要自定义扩展的类型 class propTypes extends newPropTypes {
// see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method static get style() {
propTypes.extend([ return toValidableType('style', {
{ type: [String, Object]
name: 'style', })
getter: true,
type: [String, Object],
default: undefined
} }
]) }
export { propTypes } export { propTypes }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { useDesign } from '@/hooks/web/useDesign'
import { ref } from 'vue' import { ref } from 'vue'
import { ElScrollbar } from 'element-plus' import { ElScrollbar } from 'element-plus'
import { computed } from 'vue' import { computed } from 'vue'
import { ElButton } from 'element-plus'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -28,6 +29,11 @@ const toTelephoneLogin = () => {
const toPasswordLogin = () => { const toPasswordLogin = () => {
isPasswordLogin.value = true isPasswordLogin.value = true
} }
const icpNumber = computed(() => appStore.getIcpNumber)
const toICO = () => {
window.open('https://beian.miit.gov.cn/#/Integrated/index')
}
</script> </script>
<template> <template>
@ -88,6 +94,9 @@ const toPasswordLogin = () => {
/> />
</div> </div>
</Transition> </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>
</div> </div>
</ElScrollbar> </ElScrollbar>

View File

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

View File

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

View File

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

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