升级V3
This commit is contained in:
@@ -1,22 +0,0 @@
|
|||||||
# 告诉EditorConfig插件,这是根文件,不用继续往上查找
|
|
||||||
root = true
|
|
||||||
|
|
||||||
# 匹配全部文件
|
|
||||||
[*]
|
|
||||||
# 设置字符集
|
|
||||||
charset = utf-8
|
|
||||||
# 缩进风格,可选space、tab
|
|
||||||
indent_style = space
|
|
||||||
# 缩进的空格数
|
|
||||||
indent_size = 2
|
|
||||||
# 结尾换行符,可选lf、cr、crlf
|
|
||||||
end_of_line = lf
|
|
||||||
# 在文件结尾插入新行
|
|
||||||
insert_final_newline = true
|
|
||||||
# 删除一行中的前后空格
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
# 匹配md结尾的文件
|
|
||||||
[*.md]
|
|
||||||
insert_final_newline = false
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 若依管理系统
|
VITE_APP_TITLE = 若依管理系统
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
ENV = 'development'
|
VITE_APP_ENV = 'development'
|
||||||
|
|
||||||
# 若依管理系统/开发环境
|
# 若依管理系统/开发环境
|
||||||
VUE_APP_BASE_API = '/dev-api'
|
VITE_APP_BASE_API = '/dev-api'
|
||||||
|
|
||||||
# 路由懒加载
|
|
||||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 若依管理系统
|
VITE_APP_TITLE = 若依管理系统
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
ENV = 'production'
|
VITE_APP_ENV = 'production'
|
||||||
|
|
||||||
# 若依管理系统/生产环境
|
# 若依管理系统/生产环境
|
||||||
VUE_APP_BASE_API = '/prod-api'
|
VITE_APP_BASE_API = '/prod-api'
|
||||||
|
|
||||||
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
|
VITE_BUILD_COMPRESS = gzip
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 若依管理系统
|
VITE_APP_TITLE = 若依管理系统
|
||||||
|
|
||||||
BABEL_ENV = production
|
# 生产环境配置
|
||||||
|
VITE_APP_ENV = 'staging'
|
||||||
|
|
||||||
NODE_ENV = production
|
# 若依管理系统/生产环境
|
||||||
|
VITE_APP_BASE_API = '/stage-api'
|
||||||
|
|
||||||
# 测试环境配置
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
ENV = 'staging'
|
VITE_BUILD_COMPRESS = gzip
|
||||||
|
|
||||||
# 若依管理系统/测试环境
|
|
||||||
VUE_APP_BASE_API = '/stage-api'
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2018 RuoYi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
+108
-16
@@ -1,30 +1,122 @@
|
|||||||
## 开发
|
<p align="center">
|
||||||
|
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||||
|
</p>
|
||||||
|
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.2</h1>
|
||||||
|
<h4 align="center">基于SpringBoot+Vue3前后端分离的Java快速开发框架</h4>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
|
||||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.2-brightgreen.svg"></a>
|
||||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 平台简介
|
||||||
|
|
||||||
|
* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
||||||
|
* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast) 版本。
|
||||||
|
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)
|
||||||
|
|
||||||
|
# 版本对比
|
||||||
|
|
||||||
|
RuoYi-Vue 前端项目的三个主要演进版本,方便你直观对比其技术栈差异(并行开发维护)。
|
||||||
|
|
||||||
|
| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
|
||||||
|
| **脚本语言** | JavaScript | JavaScript | TypeScript |
|
||||||
|
| **构建工具** | Vue CLI | Vite | Vite |
|
||||||
|
| **UI 组件库** | Element UI | Element Plus | Element Plus |
|
||||||
|
| **状态管理** | Vuex | Pinia | Pinia |
|
||||||
|
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
|
||||||
|
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
|
||||||
|
| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) |
|
||||||
|
|
||||||
|
## 前端运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://gitee.com/y_project/RuoYi-Vue
|
git clone https://github.com/yangzongzhuan/RuoYi-Vue3.git
|
||||||
|
|
||||||
# 进入项目目录
|
# 进入项目目录
|
||||||
cd ruoyi-ui
|
cd RuoYi-Vue3
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install
|
yarn --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
|
|
||||||
npm install --registry=https://registry.npmmirror.com
|
|
||||||
|
|
||||||
# 启动服务
|
# 启动服务
|
||||||
npm run dev
|
yarn dev
|
||||||
|
|
||||||
|
# 构建测试环境 yarn build:stage
|
||||||
|
# 构建生产环境 yarn build:prod
|
||||||
|
# 前端访问地址 http://localhost:80
|
||||||
```
|
```
|
||||||
|
|
||||||
浏览器访问 http://localhost:80
|
## 内置功能
|
||||||
|
|
||||||
## 发布
|
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||||
|
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
|
||||||
|
3. 岗位管理:配置系统用户所属担任职务。
|
||||||
|
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||||
|
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||||
|
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||||
|
7. 参数管理:对系统动态配置常用参数。
|
||||||
|
8. 通知公告:系统通知公告信息发布维护。
|
||||||
|
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||||
|
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||||
|
11. 在线用户:当前系统中活跃用户状态监控。
|
||||||
|
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||||
|
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
|
||||||
|
14. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||||
|
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||||
|
16. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||||
|
17. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||||
|
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
|
||||||
|
|
||||||
```bash
|
## 在线体验
|
||||||
# 构建测试环境
|
|
||||||
npm run build:stage
|
|
||||||
|
|
||||||
# 构建生产环境
|
- admin/admin123
|
||||||
npm run build:prod
|
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
|
||||||
```
|
|
||||||
|
演示地址:http://vue.ruoyi.vip
|
||||||
|
文档地址:http://doc.ruoyi.vip
|
||||||
|
|
||||||
|
## 演示图
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
|
||||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
## 若依前后端分离交流群
|
||||||
|
|
||||||
|
QQ群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
],
|
|
||||||
'env': {
|
|
||||||
'development': {
|
|
||||||
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
|
|
||||||
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
|
|
||||||
'plugins': ['dynamic-import-node']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,6 @@ echo.
|
|||||||
cd %~dp0
|
cd %~dp0
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
npm run build:prod
|
yarn build:prod
|
||||||
|
|
||||||
pause
|
pause
|
||||||
@@ -7,6 +7,6 @@ echo.
|
|||||||
cd %~dp0
|
cd %~dp0
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
npm install --registry=https://registry.npmmirror.com
|
yarn --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
pause
|
pause
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo.
|
echo.
|
||||||
echo [信息] 使用 Vue CLI 命令运行 Web 工程。
|
echo [信息] 使用 Vite 命令运行 Web 工程。
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
%~d0
|
%~d0
|
||||||
cd %~dp0
|
cd %~dp0
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
npm run dev
|
yarn dev
|
||||||
|
|
||||||
pause
|
pause
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
const { run } = require('runjs')
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const config = require('../vue.config.js')
|
|
||||||
const rawArgv = process.argv.slice(2)
|
|
||||||
const args = rawArgv.join(' ')
|
|
||||||
|
|
||||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
|
||||||
const report = rawArgv.includes('--report')
|
|
||||||
|
|
||||||
run(`vue-cli-service build ${args}`)
|
|
||||||
|
|
||||||
const port = 9526
|
|
||||||
const publicPath = config.publicPath
|
|
||||||
|
|
||||||
var connect = require('connect')
|
|
||||||
var serveStatic = require('serve-static')
|
|
||||||
const app = connect()
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
publicPath,
|
|
||||||
serveStatic('./dist', {
|
|
||||||
index: ['index.html', '/']
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
app.listen(port, function () {
|
|
||||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
|
||||||
if (report) {
|
|
||||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
run(`vue-cli-service build ${args}`)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
<!--[if lt IE 11
|
||||||
|
]><script>
|
||||||
|
window.location.href = "/html/ie.html";
|
||||||
|
</script><!
|
||||||
|
[endif]-->
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin: -75px 0 0 -75px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: #fff;
|
||||||
|
-webkit-animation: spin 2s linear infinite;
|
||||||
|
-ms-animation: spin 2s linear infinite;
|
||||||
|
-moz-animation: spin 2s linear infinite;
|
||||||
|
-o-animation: spin 2s linear infinite;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: #fff;
|
||||||
|
-webkit-animation: spin 3s linear infinite;
|
||||||
|
-moz-animation: spin 3s linear infinite;
|
||||||
|
-o-animation: spin 3s linear infinite;
|
||||||
|
-ms-animation: spin 3s linear infinite;
|
||||||
|
animation: spin 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: #fff;
|
||||||
|
-moz-animation: spin 1.5s linear infinite;
|
||||||
|
-o-animation: spin 1.5s linear infinite;
|
||||||
|
-ms-animation: spin 1.5s linear infinite;
|
||||||
|
-webkit-animation: spin 1.5s linear infinite;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
-ms-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
-ms-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
-ms-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
-ms-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 51%;
|
||||||
|
height: 100%;
|
||||||
|
background: #7171c6;
|
||||||
|
z-index: 1000;
|
||||||
|
-webkit-transform: translateX(0);
|
||||||
|
-ms-transform: translateX(0);
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section.section-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section.section-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader-wrapper .loader-section.section-left {
|
||||||
|
-webkit-transform: translateX(-100%);
|
||||||
|
-ms-transform: translateX(-100%);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader-wrapper .loader-section.section-right {
|
||||||
|
-webkit-transform: translateX(100%);
|
||||||
|
-ms-transform: translateX(100%);
|
||||||
|
transform: translateX(100%);
|
||||||
|
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: all 0.3s ease-out;
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader-wrapper {
|
||||||
|
visibility: hidden;
|
||||||
|
-webkit-transform: translateY(-100%);
|
||||||
|
-ms-transform: translateY(-100%);
|
||||||
|
transform: translateY(-100%);
|
||||||
|
-webkit-transition: all 0.3s 1s ease-out;
|
||||||
|
transition: all 0.3s 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-js #loader-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-js h1 {
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .load_title {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
color: #fff;
|
||||||
|
font-size: 19px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 9999999999999;
|
||||||
|
position: absolute;
|
||||||
|
top: 60%;
|
||||||
|
opacity: 1;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .load_title span {
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
#svgContainer {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div id="loader-wrapper">
|
||||||
|
<!-- <div id="loader"></div> -->
|
||||||
|
<!-- <div class="loader-section section-left"></div> -->
|
||||||
|
<!-- <div class="loader-section section-right"></div> -->
|
||||||
|
<div id="svgContainer"></div>
|
||||||
|
<!-- <div class="load_title">正在加载系统资源,请耐心等待</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
<script src="/src/plugins/lottie.min.js"></script>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
let svgContainer = document.getElementById("svgContainer");
|
||||||
|
let animItem = bodymovin.loadAnimation({
|
||||||
|
container: svgContainer,
|
||||||
|
animType: "svg",
|
||||||
|
loop: true,
|
||||||
|
path: "/src/plugins/chatbot.json",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
+33
-51
@@ -4,68 +4,50 @@
|
|||||||
"description": "若依管理系统",
|
"description": "若依管理系统",
|
||||||
"author": "若依",
|
"author": "若依",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build:prod": "vue-cli-service build",
|
"build:prod": "vite build",
|
||||||
"build:stage": "vue-cli-service build --mode staging",
|
"build:stage": "vite build --mode staging",
|
||||||
"preview": "node build/index.js --preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"keywords": [
|
|
||||||
"vue",
|
|
||||||
"admin",
|
|
||||||
"dashboard",
|
|
||||||
"element-ui",
|
|
||||||
"boilerplate",
|
|
||||||
"admin-template",
|
|
||||||
"management-system"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
|
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@riophae/vue-treeselect": "0.4.0",
|
"@element-plus/icons-vue": "2.3.2",
|
||||||
"axios": "0.30.3",
|
"@vueup/vue-quill": "1.2.0",
|
||||||
"clipboard": "2.0.8",
|
"@vueuse/core": "14.1.0",
|
||||||
"core-js": "3.37.1",
|
"axios": "1.13.2",
|
||||||
"echarts": "5.4.0",
|
"clipboard": "2.0.11",
|
||||||
"element-ui": "2.15.14",
|
"echarts": "5.6.0",
|
||||||
|
"element-plus": "2.13.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fuse.js": "6.4.3",
|
"fuse.js": "7.1.0",
|
||||||
"highlight.js": "9.18.5",
|
"js-beautify": "1.15.4",
|
||||||
"js-beautify": "1.13.0",
|
"js-cookie": "3.0.5",
|
||||||
"js-cookie": "3.0.1",
|
"jsencrypt": "3.3.2",
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"quill": "2.0.2",
|
"pinia": "3.0.4",
|
||||||
"screenfull": "5.0.2",
|
"vue": "3.5.26",
|
||||||
"sortablejs": "1.10.2",
|
"vue-cropper": "1.1.1",
|
||||||
"vue": "2.6.12",
|
"vue-router": "4.6.4",
|
||||||
"vue-count-to": "1.0.13",
|
"vuedraggable": "4.1.0"
|
||||||
"vue-cropper": "0.5.5",
|
|
||||||
"vue-router": "3.4.9",
|
|
||||||
"vuedraggable": "2.24.3",
|
|
||||||
"vuex": "3.6.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "4.4.6",
|
"@vitejs/plugin-vue": "5.2.4",
|
||||||
"@vue/cli-service": "4.4.6",
|
"sass-embedded": "1.97.2",
|
||||||
"babel-plugin-dynamic-import-node": "2.3.3",
|
"unplugin-auto-import": "0.18.6",
|
||||||
"chalk": "4.1.0",
|
"unplugin-vue-setup-extend-plus": "1.0.1",
|
||||||
"compression-webpack-plugin": "6.1.2",
|
"vite": "6.4.1",
|
||||||
"connect": "3.6.6",
|
"vite-plugin-compression": "0.5.1",
|
||||||
"sass": "1.32.13",
|
"vite-plugin-svg-icons": "2.0.1"
|
||||||
"sass-loader": "10.1.1",
|
|
||||||
"script-ext-html-webpack-plugin": "2.1.5",
|
|
||||||
"svg-sprite-loader": "5.1.1",
|
|
||||||
"vue-template-compiler": "2.6.12"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"overrides": {
|
||||||
"node": ">=8.9",
|
"quill": "2.0.2"
|
||||||
"npm": ">= 3.0.0"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"resolutions": {
|
||||||
"> 1%",
|
"quill": "2.0.2"
|
||||||
"last 2 versions"
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
||||||
<meta name="renderer" content="webkit">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title><%= webpackConfig.name %></title>
|
|
||||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
.chromeframe {
|
|
||||||
margin: 0.2em 0;
|
|
||||||
background: #ccc;
|
|
||||||
color: #000;
|
|
||||||
padding: 0.2em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
margin: -75px 0 0 -75px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-top-color: #FFF;
|
|
||||||
-webkit-animation: spin 2s linear infinite;
|
|
||||||
-ms-animation: spin 2s linear infinite;
|
|
||||||
-moz-animation: spin 2s linear infinite;
|
|
||||||
-o-animation: spin 2s linear infinite;
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
z-index: 1001;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
|
||||||
right: 5px;
|
|
||||||
bottom: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-top-color: #FFF;
|
|
||||||
-webkit-animation: spin 3s linear infinite;
|
|
||||||
-moz-animation: spin 3s linear infinite;
|
|
||||||
-o-animation: spin 3s linear infinite;
|
|
||||||
-ms-animation: spin 3s linear infinite;
|
|
||||||
animation: spin 3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 15px;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-top-color: #FFF;
|
|
||||||
-moz-animation: spin 1.5s linear infinite;
|
|
||||||
-o-animation: spin 1.5s linear infinite;
|
|
||||||
-ms-animation: spin 1.5s linear infinite;
|
|
||||||
-webkit-animation: spin 1.5s linear infinite;
|
|
||||||
animation: spin 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
-ms-transform: rotate(0deg);
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
-ms-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
-ms-transform: rotate(0deg);
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
-ms-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#loader-wrapper .loader-section {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 51%;
|
|
||||||
height: 100%;
|
|
||||||
background: #7171C6;
|
|
||||||
z-index: 1000;
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
-ms-transform: translateX(0);
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader-wrapper .loader-section.section-left {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader-wrapper .loader-section.section-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.loaded #loader-wrapper .loader-section.section-left {
|
|
||||||
-webkit-transform: translateX(-100%);
|
|
||||||
-ms-transform: translateX(-100%);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
|
||||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loaded #loader-wrapper .loader-section.section-right {
|
|
||||||
-webkit-transform: translateX(100%);
|
|
||||||
-ms-transform: translateX(100%);
|
|
||||||
transform: translateX(100%);
|
|
||||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
|
||||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loaded #loader {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transition: all 0.3s ease-out;
|
|
||||||
transition: all 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loaded #loader-wrapper {
|
|
||||||
visibility: hidden;
|
|
||||||
-webkit-transform: translateY(-100%);
|
|
||||||
-ms-transform: translateY(-100%);
|
|
||||||
transform: translateY(-100%);
|
|
||||||
-webkit-transition: all 0.3s 1s ease-out;
|
|
||||||
transition: all 0.3s 1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-js #loader-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-js h1 {
|
|
||||||
color: #222222;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader-wrapper .load_title {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
color: #FFF;
|
|
||||||
font-size: 19px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 9999999999999;
|
|
||||||
position: absolute;
|
|
||||||
top: 60%;
|
|
||||||
opacity: 1;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader-wrapper .load_title span {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #FFF;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div id="loader-wrapper">
|
|
||||||
<div id="loader"></div>
|
|
||||||
<div class="loader-section section-left"></div>
|
|
||||||
<div class="loader-section section-right"></div>
|
|
||||||
<div class="load_title">正在加载系统资源,请耐心等待</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
File diff suppressed because one or more lines are too long
+10
-15
@@ -1,20 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<router-view />
|
||||||
<router-view />
|
|
||||||
<theme-picker />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ThemePicker from "@/components/ThemePicker"
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import { handleThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
export default {
|
onMounted(() => {
|
||||||
name: "App",
|
nextTick(() => {
|
||||||
components: { ThemePicker }
|
// 初始化主题样式
|
||||||
}
|
handleThemeStyle(useSettingsStore().theme)
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
#app .theme-picker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -57,4 +57,4 @@ export function optionselect() {
|
|||||||
url: '/system/dict/type/optionselect',
|
url: '/system/dict/type/optionselect',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { parseStrEmpty } from "@/utils/ruoyi"
|
import { parseStrEmpty } from "@/utils/ruoyi";
|
||||||
|
|
||||||
// 查询用户列表
|
// 查询用户列表
|
||||||
export function listUser(query) {
|
export function listUser(query) {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import SvgIcon from '@/components/SvgIcon'// svg component
|
|
||||||
|
|
||||||
// register globally
|
|
||||||
Vue.component('svg-icon', SvgIcon)
|
|
||||||
|
|
||||||
const req = require.context('./svg', false, /\.svg$/)
|
|
||||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
|
||||||
requireAll(req)
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303018722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M368.832 67.2c51.328-16.384 89.216 34.112 75.712 76.416a346.816 346.816 0 0 0 435.84 435.84c42.304-13.44 92.8 24.384 76.48 75.712A467.968 467.968 0 1 1 368.832 67.2z m-35.776 122.688a368.832 368.832 0 1 0 501.056 501.056 445.952 445.952 0 0 1-501.056-501.056z" p-id="1448"></path></svg>
|
||||||
|
After Width: | Height: | Size: 619 B |
@@ -1 +1 @@
|
|||||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path fill="#5a5e66" d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 226 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303115132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12397" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 890.432c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 0 1-66.816 0v-66.752c0-18.432 14.976-33.408 33.408-33.408z m-267.52-110.848a33.408 33.408 0 0 1 0 47.232l-47.296 47.232a33.408 33.408 0 0 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0z m582.336 0l47.232 47.232a33.408 33.408 0 0 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 1 1 47.232-47.232zM512 200.32a311.68 311.68 0 1 1 0 623.296 311.68 311.68 0 0 1 0-623.36z m0 66.752a244.864 244.864 0 1 0 0 489.728 244.864 244.864 0 0 0 0-489.728zM100.16 478.592a33.408 33.408 0 1 1 0 66.816H33.408a33.408 33.408 0 0 1 0-66.816h66.752z m890.432 0a33.408 33.408 0 0 1 0 66.816h-66.752a33.408 33.408 0 1 1 0-66.816h66.752zM197.184 149.952l47.232 47.232a33.408 33.408 0 1 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 0 1 47.232-47.232z m676.864 0a33.408 33.408 0 0 1 0 47.232l-47.232 47.232a33.408 33.408 0 1 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0zM512 0c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 1 1-66.816 0V33.408C478.592 14.976 493.568 0 512 0z" p-id="12398"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,22 +0,0 @@
|
|||||||
# replace default config
|
|
||||||
|
|
||||||
# multipass: true
|
|
||||||
# full: true
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
# - name
|
|
||||||
#
|
|
||||||
# or:
|
|
||||||
# - name: false
|
|
||||||
# - name: true
|
|
||||||
#
|
|
||||||
# or:
|
|
||||||
# - name:
|
|
||||||
# param1: 1
|
|
||||||
# param2: 2
|
|
||||||
|
|
||||||
- removeAttrs:
|
|
||||||
attrs:
|
|
||||||
- 'fill'
|
|
||||||
- 'fill-rule'
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import './variables.scss';
|
@use './variables.module.scss' as *;
|
||||||
|
|
||||||
@mixin colorBtn($color) {
|
@mixin colorBtn($color) {
|
||||||
background: $color;
|
background: $color;
|
||||||
|
|||||||
@@ -89,4 +89,8 @@
|
|||||||
> .el-submenu__title
|
> .el-submenu__title
|
||||||
.el-submenu__icon-arrow {
|
.el-submenu__icon-arrow {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown .el-dropdown-link{
|
||||||
|
color: var(--el-color-primary) !important;
|
||||||
}
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* I think element-ui's default theme color is too light for long-term use.
|
|
||||||
* So I modified the default color and you can modify it to your liking.
|
|
||||||
**/
|
|
||||||
|
|
||||||
/* theme color */
|
|
||||||
$--color-primary: #1890ff;
|
|
||||||
$--color-success: #13ce66;
|
|
||||||
$--color-warning: #ffba00;
|
|
||||||
$--color-danger: #ff4949;
|
|
||||||
// $--color-info: #1E1E1E;
|
|
||||||
|
|
||||||
$--button-font-weight: 400;
|
|
||||||
|
|
||||||
// $--color-text-regular: #1f2d3d;
|
|
||||||
|
|
||||||
$--border-color-light: #dfe4ed;
|
|
||||||
$--border-color-lighter: #e6ebf5;
|
|
||||||
|
|
||||||
$--table-border: 1px solid #dfe6ec;
|
|
||||||
|
|
||||||
/* icon font path, required */
|
|
||||||
$--font-path: '~element-ui/lib/theme-chalk/fonts';
|
|
||||||
|
|
||||||
@import "~element-ui/packages/theme-chalk/src/index";
|
|
||||||
|
|
||||||
// the :export directive is the magic sauce for webpack
|
|
||||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
|
||||||
:export {
|
|
||||||
theme: $--color-primary;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
@import './variables.scss';
|
@use './mixin.scss';
|
||||||
@import './mixin.scss';
|
@use './transition.scss';
|
||||||
@import './transition.scss';
|
@use './element-ui.scss';
|
||||||
@import './element-ui.scss';
|
@use './sidebar.scss';
|
||||||
@import './sidebar.scss';
|
@use './btn.scss';
|
||||||
@import './btn.scss';
|
@use './ruoyi.scss';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
|||||||
@@ -1,69 +1,56 @@
|
|||||||
/**
|
/**
|
||||||
* 通用css样式布局处理
|
* 通用css样式布局处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 ruoyi
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 基础通用 **/
|
/** 基础通用 **/
|
||||||
.pt5 {
|
.pt5 {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pr5 {
|
.pr5 {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pb5 {
|
.pb5 {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt5 {
|
.mt5 {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr5 {
|
.mr5 {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb5 {
|
.mb5 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb8 {
|
.mb8 {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml5 {
|
.ml5 {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt10 {
|
.mt10 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr10 {
|
.mr10 {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb10 {
|
.mb10 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.ml10 {
|
.ml10 {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt20 {
|
.mt20 {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr20 {
|
.mr20 {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb20 {
|
.mb20 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.ml20 {
|
.ml20 {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||||
@@ -73,19 +60,22 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-message-box__status + .el-message-box__message{
|
.el-form--inline {
|
||||||
word-break: break-word;
|
.el-form-item {
|
||||||
|
.el-input, .el-cascader, .el-select, .el-autocomplete {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-form .el-form-item__label {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
.el-dialog:not(.is-fullscreen) {
|
.el-dialog:not(.is-fullscreen) {
|
||||||
margin-top: 6vh !important;
|
margin-top: 6vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog.scrollbar .el-dialog__body {
|
||||||
padding: 8px 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body {
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
@@ -96,13 +86,12 @@
|
|||||||
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
|
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
|
||||||
th {
|
th {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background-color: #f8f8f9;
|
background-color: #f8f8f9 !important;
|
||||||
color: #515a6e;
|
color: #515a6e;
|
||||||
height: 40px;
|
height: 40px !important;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
.el-button [class*="el-icon-"] + span {
|
.el-button [class*="el-icon-"] + span {
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
@@ -112,11 +101,11 @@
|
|||||||
|
|
||||||
/** 表单布局 **/
|
/** 表单布局 **/
|
||||||
.form-header {
|
.form-header {
|
||||||
font-size: 15px;
|
font-size:15px;
|
||||||
color: #6379bb;
|
color:#6379bb;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom:1px solid #ddd;
|
||||||
margin: 8px 10px 25px 10px;
|
margin:8px 10px 25px 10px;
|
||||||
padding-bottom: 5px
|
padding-bottom:5px
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 表格布局 **/
|
/** 表格布局 **/
|
||||||
@@ -124,19 +113,52 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗中的分页器 */
|
||||||
|
.el-dialog .pagination-container {
|
||||||
|
position: static !important;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pagination-container {
|
||||||
|
.el-pagination {
|
||||||
|
> .el-pagination__jump {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
> .el-pagination__sizes {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tree border */
|
/* tree border */
|
||||||
.tree-border {
|
.tree-border {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
border: 1px solid #e5e6e7;
|
border: 1px solid var(--el-border-color-light, #e5e6e7);
|
||||||
background: #FFFFFF none;
|
background: var(--el-bg-color, #FFFFFF) none;
|
||||||
border-radius: 4px;
|
border-radius:4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .fixed-width .el-button--small {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
width: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* horizontal el menu */
|
/* horizontal el menu */
|
||||||
.el-menu--horizontal .el-menu-item .svg-icon + span,
|
.el-menu--horizontal .el-menu-item .svg-icon + span,
|
||||||
.el-menu--horizontal .el-submenu__title .svg-icon + span {
|
.el-menu--horizontal .el-sub-menu__title .svg-icon + span {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,25 +166,11 @@
|
|||||||
min-width: 120px !important;
|
min-width: 120px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.pagination-container .el-pagination > .el-pagination__jump {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.pagination-container .el-pagination > .el-pagination__sizes {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .fixed-width .el-button--mini {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
width: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 表格更多操作下拉样式 */
|
/** 表格更多操作下拉样式 */
|
||||||
.el-table .el-dropdown-link,.el-table .el-dropdown-selfdefine {
|
.el-table .el-dropdown-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 5px;
|
color: #409EFF;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table .el-dropdown, .el-icon-arrow-down {
|
.el-table .el-dropdown, .el-icon-arrow-down {
|
||||||
@@ -199,12 +207,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-card__header {
|
.el-card__header {
|
||||||
padding: 14px 15px 7px;
|
padding: 14px 15px 7px !important;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-card__body {
|
.el-card__body {
|
||||||
padding: 15px 20px 20px 20px;
|
padding: 15px 20px 20px 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-box {
|
.card-box {
|
||||||
@@ -234,6 +242,9 @@
|
|||||||
|
|
||||||
/** 详细卡片样式 */
|
/** 详细卡片样式 */
|
||||||
.detail-drawer {
|
.detail-drawer {
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.el-drawer__header {
|
.el-drawer__header {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
padding: 8px 12px 6px;
|
padding: 8px 12px 6px;
|
||||||
@@ -457,10 +468,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar-upload-preview {
|
.avatar-upload-preview {
|
||||||
position: relative;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
transform: translate(50%, -50%);
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -472,13 +482,13 @@
|
|||||||
.allowDrag { cursor: grab; }
|
.allowDrag { cursor: grab; }
|
||||||
.allowDrag:active { cursor: grabbing; }
|
.allowDrag:active { cursor: grabbing; }
|
||||||
|
|
||||||
.sortable-ghost {
|
.sortable-ghost{
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
color: #fff !important;
|
color: #fff!important;
|
||||||
background: #42b983 !important;
|
background: #42b983!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格右侧工具栏样式 */
|
||||||
.top-right-btn {
|
.top-right-btn {
|
||||||
position: relative;
|
margin-left: auto;
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
@use './variables.module.scss' as vars;
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
transition: margin-left .28s;
|
transition: margin-left .28s;
|
||||||
margin-left: $base-sidebar-width;
|
margin-left: vars.$base-sidebar-width;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,10 +14,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
-webkit-transition: width .28s;
|
|
||||||
transition: width 0.28s;
|
transition: width 0.28s;
|
||||||
width: $base-sidebar-width !important;
|
width: vars.$base-sidebar-width !important;
|
||||||
background-color: $base-menu-background;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item, .el-submenu__title {
|
.el-menu-item, .menu-title {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
@@ -78,33 +78,37 @@
|
|||||||
line-height: 44px !important;
|
line-height: 44px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-menu-item .el-menu-tooltip__trigger {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
// menu hover
|
// menu hover
|
||||||
.submenu-title-noDropdown,
|
.sub-menu-title-noDropdown,
|
||||||
.el-submenu__title {
|
.el-sub-menu__title {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .theme-dark .is-active > .el-submenu__title {
|
& .theme-dark .is-active > .el-sub-menu__title {
|
||||||
color: $base-menu-color-active !important;
|
color: vars.$base-menu-color-active !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .nest-menu .el-submenu>.el-submenu__title,
|
& .nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||||
& .el-submenu .el-menu-item {
|
& .el-sub-menu .el-menu-item {
|
||||||
min-width: $base-sidebar-width !important;
|
min-width: vars.$base-sidebar-width !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .theme-dark .nest-menu .el-submenu>.el-submenu__title,
|
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||||
& .theme-dark .el-submenu .el-menu-item {
|
& .theme-dark .el-sub-menu .el-menu-item {
|
||||||
background-color: $base-sub-menu-background !important;
|
background-color: vars.$base-sub-menu-background;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $base-sub-menu-hover !important;
|
background-color: vars.$base-sub-menu-hover !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,19 +125,18 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
|
background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
|
||||||
border-right: 3px solid var(--current-color, #409eff);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-submenu.is-active > .el-submenu__title {
|
.el-sub-menu.is-active > .el-sub-menu__title {
|
||||||
color: var(--current-color, #409eff) !important;
|
color: var(--current-color, #409eff) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:not(.is-active),
|
.el-menu-item:not(.is-active),
|
||||||
.submenu-title-noDropdown,
|
.submenu-title-noDropdown,
|
||||||
.el-submenu__title {
|
.el-sub-menu__title {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@@ -158,7 +161,7 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-submenu__title {
|
.el-sub-menu__title {
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,29 +174,28 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: var(--current-color-light, #ecf5ff);
|
background-color: var(--current-color-light, #ecf5ff);
|
||||||
border-right: 3px solid var(--current-color, #409eff);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-submenu.is-active > .el-submenu__title {
|
.el-sub-menu.is-active > .el-sub-menu__title {
|
||||||
color: var(--current-color, #409eff) !important;
|
color: var(--current-color, #409eff) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:not(.is-active):hover,
|
.el-menu-item:not(.is-active):hover,
|
||||||
.submenu-title-noDropdown:hover,
|
.submenu-title-noDropdown:hover,
|
||||||
.el-submenu__title:hover {
|
.el-sub-menu__title:hover {
|
||||||
background-color: #f5f7fa !important;
|
background-color: #f5f7fa;
|
||||||
color: rgba(0, 0, 0, 0.85) !important;
|
color: rgba(0, 0, 0, 0.85) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nest-menu .el-submenu > .el-submenu__title,
|
.nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||||
.el-submenu .el-menu-item {
|
.el-sub-menu .el-menu-item {
|
||||||
background-color: #fafafa !important;
|
background-color: #fafafa;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f0f5ff !important;
|
background-color: #f0f5ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,25 +210,23 @@
|
|||||||
margin-left: 54px;
|
margin-left: 54px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu:not(.el-menu--horizontal) {
|
.sub-menu-title-noDropdown {
|
||||||
.submenu-title-noDropdown {
|
padding: 0 !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.el-tooltip {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.el-tooltip {
|
.svg-icon {
|
||||||
padding: 0 !important;
|
margin-left: 20px;
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-submenu {
|
.el-sub-menu {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&>.el-submenu__title {
|
&>.el-sub-menu__title {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
@@ -237,8 +237,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
.el-submenu {
|
.el-sub-menu {
|
||||||
&>.el-submenu__title {
|
&>.el-sub-menu__title {
|
||||||
&>span {
|
&>span {
|
||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
@@ -246,13 +246,20 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
&>i {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu--collapse .el-menu .el-submenu {
|
.el-menu--collapse .el-menu .el-sub-menu {
|
||||||
min-width: $base-sidebar-width !important;
|
min-width: vars.$base-sidebar-width !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mobile responsive
|
// mobile responsive
|
||||||
@@ -263,14 +270,14 @@
|
|||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
transition: transform .28s;
|
transition: transform .28s;
|
||||||
width: $base-sidebar-width !important;
|
width: vars.$base-sidebar-width !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hideSidebar {
|
&.hideSidebar {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
transform: translate3d(-$base-sidebar-width, 0, 0);
|
transform: translate3d(-(vars.$base-sidebar-width), 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,15 +299,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nest-menu .el-submenu>.el-submenu__title,
|
.nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
&:hover {
|
&:hover {
|
||||||
// you can use $subMenuHover
|
// you can use $sub-menuHover
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
background-color: rgba(0, 0, 0, 0.06) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the scroll bar appears when the subMenu is too long
|
// the scroll bar appears when the sub-menu is too long
|
||||||
>.el-menu--popup {
|
>.el-menu--popup {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
transition: opacity 0.28s;
|
transition: opacity 0.28s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter,
|
.fade-enter-from,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
transition: all .5s;
|
transition: all .5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-transform-enter {
|
.fade-transform-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-30px);
|
transform: translateX(-30px);
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
transition: all .5s;
|
transition: all .5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-enter,
|
.breadcrumb-enter-from,
|
||||||
.breadcrumb-leave-active {
|
.breadcrumb-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
@@ -47,3 +47,34 @@
|
|||||||
.breadcrumb-leave-active {
|
.breadcrumb-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 黑暗模式下过渡效果 */
|
||||||
|
::view-transition-new(root), ::view-transition-old(root) {
|
||||||
|
animation: none !important;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark::view-transition-old(root) {
|
||||||
|
z-index: 2147483646;
|
||||||
|
background: var(--bg-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
z-index: 2147483646;
|
||||||
|
background: var(--bg-color-dark);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
// base color
|
||||||
|
$blue: #324157;
|
||||||
|
$light-blue: #333c46;
|
||||||
|
$red: #C03639;
|
||||||
|
$pink: #E65D6E;
|
||||||
|
$green: #30B08F;
|
||||||
|
$tiffany: #4AB7BD;
|
||||||
|
$yellow: #FEC171;
|
||||||
|
$panGreen: #30B08F;
|
||||||
|
|
||||||
|
// 默认主题变量
|
||||||
|
$menuText: #bfcbd9;
|
||||||
|
$menuActiveText: #409eff;
|
||||||
|
$menuBg: #1a1f2e;
|
||||||
|
$menuHover: #263445;
|
||||||
|
|
||||||
|
// 浅色主题theme-light
|
||||||
|
$menuLightBg: #ffffff;
|
||||||
|
$menuLightHover: #f0f1f5;
|
||||||
|
$menuLightText: #303133;
|
||||||
|
$menuLightActiveText: #409EFF;
|
||||||
|
|
||||||
|
// 基础变量
|
||||||
|
$base-sidebar-width: 200px;
|
||||||
|
$sideBarWidth: 200px;
|
||||||
|
|
||||||
|
// 菜单暗色变量
|
||||||
|
$base-menu-color: rgba(255,255,255,.65);
|
||||||
|
$base-menu-color-active: #ffffff;
|
||||||
|
$base-menu-background: #1a1f2e;
|
||||||
|
$base-sub-menu-background: #141824;
|
||||||
|
$base-sub-menu-hover: rgba(255,255,255,.06);
|
||||||
|
|
||||||
|
// 组件变量
|
||||||
|
$--color-primary: #409EFF;
|
||||||
|
$--color-success: #67C23A;
|
||||||
|
$--color-warning: #E6A23C;
|
||||||
|
$--color-danger: #F56C6C;
|
||||||
|
$--color-info: #909399;
|
||||||
|
|
||||||
|
:export {
|
||||||
|
menuText: $menuText;
|
||||||
|
menuActiveText: $menuActiveText;
|
||||||
|
menuBg: $menuBg;
|
||||||
|
menuHover: $menuHover;
|
||||||
|
menuLightBg: $menuLightBg;
|
||||||
|
menuLightHover: $menuLightHover;
|
||||||
|
menuLightText: $menuLightText;
|
||||||
|
menuLightActiveText: $menuLightActiveText;
|
||||||
|
sideBarWidth: $sideBarWidth;
|
||||||
|
// 导出基础颜色
|
||||||
|
blue: $blue;
|
||||||
|
lightBlue: $light-blue;
|
||||||
|
red: $red;
|
||||||
|
pink: $pink;
|
||||||
|
green: $green;
|
||||||
|
tiffany: $tiffany;
|
||||||
|
yellow: $yellow;
|
||||||
|
panGreen: $panGreen;
|
||||||
|
// 导出组件颜色
|
||||||
|
colorPrimary: $--color-primary;
|
||||||
|
colorSuccess: $--color-success;
|
||||||
|
colorWarning: $--color-warning;
|
||||||
|
colorDanger: $--color-danger;
|
||||||
|
colorInfo: $--color-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS变量定义
|
||||||
|
:root {
|
||||||
|
/* 亮色模式变量 */
|
||||||
|
--sidebar-bg: #{$menuBg};
|
||||||
|
--sidebar-text: #{$menuText};
|
||||||
|
--menu-hover: #{$menuHover};
|
||||||
|
|
||||||
|
--navbar-bg: #ffffff;
|
||||||
|
--navbar-text: #303133;
|
||||||
|
|
||||||
|
/* splitpanes default-theme 变量 */
|
||||||
|
--splitpanes-default-bg: #ffffff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暗黑模式变量
|
||||||
|
html.dark {
|
||||||
|
/* 默认通用 */
|
||||||
|
--el-bg-color: #141414;
|
||||||
|
--el-bg-color-overlay: #1d1e1f;
|
||||||
|
--el-text-color-primary: #ffffff;
|
||||||
|
--el-text-color-regular: #d0d0d0;
|
||||||
|
--el-border-color: #434343;
|
||||||
|
--el-border-color-light: #434343;
|
||||||
|
--el-menu-text-color: #d0d0d0;
|
||||||
|
--sidebar-logo-text: #d0d0d0;
|
||||||
|
|
||||||
|
/* primary */
|
||||||
|
--primary-bg: #18212b;
|
||||||
|
|
||||||
|
/* 侧边栏 */
|
||||||
|
--sidebar-bg: #141414;
|
||||||
|
--sidebar-text: #ffffff;
|
||||||
|
--menu-hover: #2d2d2d;
|
||||||
|
--menu-active-text: #{$menuActiveText};
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
--navbar-bg: #141414;
|
||||||
|
--navbar-text: #ffffff;
|
||||||
|
--navbar-hover: #141414;
|
||||||
|
|
||||||
|
/* 标签栏 */
|
||||||
|
--tags-bg: #141414;
|
||||||
|
--tags-item-bg: #1d1e1f;
|
||||||
|
--tags-item-border: #303030;
|
||||||
|
--tags-item-text: #d0d0d0;
|
||||||
|
--tags-item-hover: #2d2d2d;
|
||||||
|
--tags-close-hover: #64666a;
|
||||||
|
/* 卡片模式激活页签 */
|
||||||
|
--tags-card-active-bg: var(--el-bg-color-overlay);
|
||||||
|
--tags-card-active-bg: color-mix(in srgb, var(--el-color-primary) 22%, var(--el-bg-color-overlay) 78%);
|
||||||
|
--tags-card-active-border: var(--tags-card-active-bg);
|
||||||
|
|
||||||
|
/* splitpanes 组件暗黑模式变量 */
|
||||||
|
--splitpanes-bg: #141414;
|
||||||
|
--splitpanes-border: #303030;
|
||||||
|
--splitpanes-splitter-bg: #1d1e1f;
|
||||||
|
--splitpanes-splitter-hover-bg: #2d2d2d;
|
||||||
|
|
||||||
|
/* blockquote 暗黑模式变量 */
|
||||||
|
--blockquote-bg: #1d1e1f;
|
||||||
|
--blockquote-border: #303030;
|
||||||
|
--blockquote-text: #d0d0d0;
|
||||||
|
|
||||||
|
/* Cron 时间表达式 模式变量 */
|
||||||
|
--cron-border: #303030;
|
||||||
|
|
||||||
|
/* splitpanes default-theme 暗黑模式变量 */
|
||||||
|
--splitpanes-default-bg: #141414;
|
||||||
|
|
||||||
|
/* 侧边栏菜单覆盖 */
|
||||||
|
.sidebar-container {
|
||||||
|
.el-menu-item:not(.is-active), .menu-title {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
& .el-menu .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||||
|
& .el-menu .theme-dark .el-sub-menu .el-menu-item,
|
||||||
|
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||||
|
& .theme-dark .el-sub-menu .el-menu-item {
|
||||||
|
background-color: var(--el-bg-color) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.theme-light {
|
||||||
|
border-right: none !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: var(--sidebar-text) !important;
|
||||||
|
}
|
||||||
|
.el-menu-item:not(.is-active):hover,
|
||||||
|
.submenu-title-noDropdown:hover,
|
||||||
|
.el-sub-menu__title:hover {
|
||||||
|
background-color: var(--menu-hover) !important;
|
||||||
|
}
|
||||||
|
.el-menu .nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||||
|
.el-menu .el-sub-menu .el-menu-item,
|
||||||
|
.nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||||
|
.el-sub-menu .el-menu-item {
|
||||||
|
background-color: var(--el-bg-color) !important;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu-container {
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu .el-sub-menu__title {
|
||||||
|
color: var(--el-text-color-regular) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title{
|
||||||
|
color: var(--el-text-color-regular) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏栏菜单覆盖 */
|
||||||
|
.el-menu--horizontal {
|
||||||
|
.el-menu-item, .el-sub-menu {
|
||||||
|
&:not(.is-disabled) {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--navbar-hover) !important;
|
||||||
|
.el-sub-menu__title {
|
||||||
|
background-color: var(--navbar-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页签样式覆盖 */
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 1px 4px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
.tags-view-container {
|
||||||
|
&.tags-view-container--chrome {
|
||||||
|
--chrome-strip-bg: var(--el-bg-color);
|
||||||
|
--chrome-strip-border: var(--el-border-color);
|
||||||
|
--chrome-tab-active-bg: var(--el-bg-color-overlay);
|
||||||
|
--chrome-tab-active-bg: color-mix(in srgb, var(--el-color-primary) 22%, var(--el-bg-color-overlay) 78%);
|
||||||
|
--chrome-tab-text: var(--el-text-color-secondary);
|
||||||
|
--chrome-tab-text-active: var(--el-color-primary);
|
||||||
|
.tags-view-wrapper .tags-view-item:not(.active) + .tags-view-item:not(.active) {
|
||||||
|
border-left-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
.tags-view-wrapper .tags-view-item {
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background: var(--el-fill-color, #303030) !important;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
|
||||||
|
background-color: var(--tags-card-active-bg) !important;
|
||||||
|
border-color: var(--tags-card-active-border) !important;
|
||||||
|
color: var(--current-color, #409eff) !important;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: var(--current-color, #409eff) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割窗格覆盖 */
|
||||||
|
.tree-sidebar-manage-wrap {
|
||||||
|
.tree-sidebar-content {
|
||||||
|
background: var(--splitpanes-bg);
|
||||||
|
}
|
||||||
|
.tree-sidebar {
|
||||||
|
border-right: 1px solid var(--splitpanes-splitter-bg);
|
||||||
|
background: var(--splitpanes-bg);
|
||||||
|
.tree-header {
|
||||||
|
background: var(--splitpanes-bg);
|
||||||
|
border-color: var(--splitpanes-border);
|
||||||
|
}
|
||||||
|
.tree-title {
|
||||||
|
color: var(--el-color-primary-light-2);
|
||||||
|
}
|
||||||
|
.collapse-button-container {
|
||||||
|
background: var(--splitpanes-bg) !important;
|
||||||
|
}
|
||||||
|
.collapse-button {
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary-light-2);
|
||||||
|
background: var(--splitpanes-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.resize-handle {
|
||||||
|
&:hover {
|
||||||
|
background: var(--splitpanes-splitter-hover-bg);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: var(--splitpanes-splitter-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式覆盖 */
|
||||||
|
.el-button--primary.is-plain {
|
||||||
|
background-color: var(--primary-bg);
|
||||||
|
border: 1px solid var(--el-color-primary-light-2);
|
||||||
|
color: var(--el-color-primary-light-2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-button-hover-bg-color);
|
||||||
|
border-color: var(--el-button-hover-border-color);
|
||||||
|
color: var(--el-button-hover-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
background-color: var(--link-active-bg-color);
|
||||||
|
border-color: var(--el-color-primary-light-3);
|
||||||
|
color: var(--el-color-primary-light-3);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* primary tag 样式覆盖 */
|
||||||
|
.el-tag--primary {
|
||||||
|
background-color: var(--primary-bg);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式覆盖 */
|
||||||
|
.el-table {
|
||||||
|
--el-table-header-bg-color: var(--el-bg-color-overlay) !important;
|
||||||
|
--el-table-header-text-color: var(--el-text-color-regular) !important;
|
||||||
|
--el-table-border-color: var(--el-border-color-light) !important;
|
||||||
|
--el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
|
||||||
|
|
||||||
|
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
|
||||||
|
th {
|
||||||
|
background-color: var(--el-bg-color-overlay, #f8f8f9) !important;
|
||||||
|
color: var(--el-text-color-regular, #515a6e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 树组件高亮样式覆盖 */
|
||||||
|
.el-tree {
|
||||||
|
.el-tree-node.is-current > .el-tree-node__content {
|
||||||
|
background-color: var(--el-bg-color-overlay) !important;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tree-node__content:hover {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单样式覆盖 */
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{
|
||||||
|
background-color: var(--navbar-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* blockquote样式覆盖 */
|
||||||
|
blockquote {
|
||||||
|
background-color: var(--blockquote-bg) !important;
|
||||||
|
border-left-color: var(--blockquote-border) !important;
|
||||||
|
color: var(--blockquote-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间表达式标题样式覆盖 */
|
||||||
|
.popup-result .title {
|
||||||
|
background: var(--cron-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部版权样式覆盖 */
|
||||||
|
.copyright {
|
||||||
|
background-color: var(--el-bg-color) !important;
|
||||||
|
color: var(--el-text-color-regular) !important;
|
||||||
|
border-top: 1px solid var(--el-bg-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// base color
|
|
||||||
$blue:#324157;
|
|
||||||
$light-blue:#3A71A8;
|
|
||||||
$red:#C03639;
|
|
||||||
$pink: #E65D6E;
|
|
||||||
$green: #30B08F;
|
|
||||||
$tiffany: #4AB7BD;
|
|
||||||
$yellow:#FEC171;
|
|
||||||
$panGreen: #30B08F;
|
|
||||||
|
|
||||||
// 默认菜单主题风格
|
|
||||||
$base-menu-color: #bfcbd9;
|
|
||||||
$base-menu-color-active: #ffffff;
|
|
||||||
$base-menu-background: #1a1f2e;
|
|
||||||
$base-logo-title-color: #ffffff;
|
|
||||||
|
|
||||||
$base-menu-light-color:rgba(0,0,0,.70);
|
|
||||||
$base-menu-light-background:#ffffff;
|
|
||||||
$base-logo-light-title-color: #001529;
|
|
||||||
|
|
||||||
$base-sub-menu-background: #141824;
|
|
||||||
$base-sub-menu-hover: rgba(255,255,255,.06);
|
|
||||||
|
|
||||||
$base-sidebar-width: 200px;
|
|
||||||
|
|
||||||
// the :export directive is the magic sauce for webpack
|
|
||||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
|
||||||
:export {
|
|
||||||
menuColor: $base-menu-color;
|
|
||||||
menuLightColor: $base-menu-light-color;
|
|
||||||
menuColorActive: $base-menu-color-active;
|
|
||||||
menuBackground: $base-menu-background;
|
|
||||||
menuLightBackground: $base-menu-light-background;
|
|
||||||
subMenuBackground: $base-sub-menu-background;
|
|
||||||
subMenuHover: $base-sub-menu-hover;
|
|
||||||
sideBarWidth: $base-sidebar-width;
|
|
||||||
logoTitleColor: $base-logo-title-color;
|
|
||||||
logoLightTitleColor: $base-logo-light-title-color
|
|
||||||
}
|
|
||||||
@@ -9,94 +9,89 @@
|
|||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
data() {
|
|
||||||
return {
|
const route = useRoute()
|
||||||
levelList: null
|
const router = useRouter()
|
||||||
}
|
const permissionStore = usePermissionStore()
|
||||||
},
|
const levelList = ref([])
|
||||||
watch: {
|
|
||||||
$route(route) {
|
function getBreadcrumb() {
|
||||||
// if you go to the redirect page, do not update the breadcrumbs
|
// only show routes with meta.title
|
||||||
if (route.path.startsWith('/redirect/')) {
|
let matched = []
|
||||||
return
|
const pathNum = findPathNum(route.path)
|
||||||
}
|
// multi-level menu
|
||||||
this.getBreadcrumb()
|
if (pathNum > 2) {
|
||||||
}
|
const reg = /\/\w+/gi
|
||||||
},
|
const pathList = route.path.match(reg).map((item, index) => {
|
||||||
created() {
|
if (index !== 0) item = item.slice(1)
|
||||||
this.getBreadcrumb()
|
return item
|
||||||
},
|
})
|
||||||
methods: {
|
getMatched(pathList, permissionStore.defaultRoutes, matched)
|
||||||
getBreadcrumb() {
|
} else {
|
||||||
// only show routes with meta.title
|
matched = route.matched.filter((item) => item.meta && item.meta.title)
|
||||||
let matched = []
|
}
|
||||||
const router = this.$route
|
// 判断是否为首页
|
||||||
const pathNum = this.findPathNum(router.path)
|
if (!isDashboard(matched[0])) {
|
||||||
// multi-level menu
|
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
|
||||||
if (pathNum > 2) {
|
}
|
||||||
const reg = /\/\w+/gi
|
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||||
const pathList = router.path.match(reg).map((item, index) => {
|
}
|
||||||
if (index !== 0) item = item.slice(1)
|
function findPathNum(str, char = "/") {
|
||||||
return item
|
let index = str.indexOf(char)
|
||||||
})
|
let num = 0
|
||||||
this.getMatched(pathList, this.$store.getters.defaultRoutes, matched)
|
while (index !== -1) {
|
||||||
} else {
|
num++
|
||||||
matched = router.matched.filter(item => item.meta && item.meta.title)
|
index = str.indexOf(char, index + 1)
|
||||||
}
|
}
|
||||||
// 判断是否为首页
|
return num
|
||||||
if (!this.isDashboard(matched[0])) {
|
}
|
||||||
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
|
function getMatched(pathList, routeList, matched) {
|
||||||
}
|
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
|
||||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
if (data) {
|
||||||
},
|
matched.push(data)
|
||||||
findPathNum(str, char = "/") {
|
if (data.children && pathList.length) {
|
||||||
let index = str.indexOf(char)
|
pathList.shift()
|
||||||
let num = 0
|
getMatched(pathList, data.children, matched)
|
||||||
while (index !== -1) {
|
|
||||||
num++
|
|
||||||
index = str.indexOf(char, index + 1)
|
|
||||||
}
|
|
||||||
return num
|
|
||||||
},
|
|
||||||
getMatched(pathList, routeList, matched) {
|
|
||||||
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
|
|
||||||
if (data) {
|
|
||||||
matched.push(data)
|
|
||||||
if (data.children && pathList.length) {
|
|
||||||
pathList.shift()
|
|
||||||
this.getMatched(pathList, data.children, matched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDashboard(route) {
|
|
||||||
const name = route && route.name
|
|
||||||
if (!name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return name.trim() === 'Index'
|
|
||||||
},
|
|
||||||
handleLink(item) {
|
|
||||||
const { redirect, path } = item
|
|
||||||
if (redirect) {
|
|
||||||
this.$router.push(redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$router.push(path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function isDashboard(route) {
|
||||||
|
const name = route && route.name
|
||||||
|
if (!name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return name.trim() === 'Index'
|
||||||
|
}
|
||||||
|
function handleLink(item) {
|
||||||
|
const { redirect, path } = item
|
||||||
|
if (redirect) {
|
||||||
|
router.push(redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// if you go to the redirect page, do not update the breadcrumbs
|
||||||
|
if (route.path.startsWith('/redirect/')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getBreadcrumb()
|
||||||
|
})
|
||||||
|
getBreadcrumb()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
.app-breadcrumb.el-breadcrumb {
|
.app-breadcrumb.el-breadcrumb {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
|
|
||||||
.no-redirect {
|
.no-redirect {
|
||||||
color: #97a8be;
|
color: #97a8be;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,161 +1,174 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size="small">
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
日,允许的通配符[, - * ? / L W]
|
日,允许的通配符[, - * ? / L W]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
不指定
|
不指定
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min="1" :max="30" /> -
|
<el-input-number v-model='cycle01' :min="1" :max="30" /> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" /> 日
|
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" /> 日
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="4">
|
<el-radio v-model='radioValue' :value="4">
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
|
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
|
||||||
<el-input-number v-model='average02' :min="1" :max="31 - average01 || 1" /> 日执行一次
|
<el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="5">
|
<el-radio v-model='radioValue' :value="5">
|
||||||
每月
|
每月
|
||||||
<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
|
<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="6">
|
<el-radio v-model='radioValue' :value="6">
|
||||||
本月最后一天
|
本月最后一天
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="7">
|
<el-radio v-model='radioValue' :value="7">
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
|
||||||
<el-option v-for="item in 31" :key="item" :value="item">{{item}}</el-option>
|
<el-option v-for="item in 31" :key="item" :label="item" :value="item" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script>
|
const emit = defineEmits(['update'])
|
||||||
export default {
|
const props = defineProps({
|
||||||
data() {
|
cron: {
|
||||||
return {
|
type: Object,
|
||||||
radioValue: 1,
|
default: {
|
||||||
workday: 1,
|
second: "*",
|
||||||
cycle01: 1,
|
min: "*",
|
||||||
cycle02: 2,
|
hour: "*",
|
||||||
average01: 1,
|
day: "*",
|
||||||
average02: 1,
|
month: "*",
|
||||||
checkboxList: [],
|
week: "?",
|
||||||
checkNum: this.$options.propsData.check
|
year: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
name: 'crontab-day',
|
check: {
|
||||||
props: ['check', 'cron'],
|
type: Function,
|
||||||
methods: {
|
default: () => {
|
||||||
// 单选按钮值变化时
|
}
|
||||||
radioChange() {
|
}
|
||||||
('day rachange')
|
})
|
||||||
if (this.radioValue !== 2 && this.cron.week !== '?') {
|
const radioValue = ref(1)
|
||||||
this.$emit('update', 'week', '?', 'day')
|
const cycle01 = ref(1)
|
||||||
}
|
const cycle02 = ref(2)
|
||||||
|
const average01 = ref(1)
|
||||||
switch (this.radioValue) {
|
const average02 = ref(1)
|
||||||
case 1:
|
const workday = ref(1)
|
||||||
this.$emit('update', 'day', '*')
|
const checkboxList = ref([])
|
||||||
break
|
const checkCopy = ref([1])
|
||||||
case 2:
|
const cycleTotal = computed(() => {
|
||||||
this.$emit('update', 'day', '?')
|
cycle01.value = props.check(cycle01.value, 1, 30)
|
||||||
break
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
|
||||||
case 3:
|
return cycle01.value + '-' + cycle02.value
|
||||||
this.$emit('update', 'day', this.cycleTotal)
|
})
|
||||||
break
|
const averageTotal = computed(() => {
|
||||||
case 4:
|
average01.value = props.check(average01.value, 1, 30)
|
||||||
this.$emit('update', 'day', this.averageTotal)
|
average02.value = props.check(average02.value, 1, 31 - average01.value)
|
||||||
break
|
return average01.value + '/' + average02.value
|
||||||
case 5:
|
})
|
||||||
this.$emit('update', 'day', this.workday + 'W')
|
const workdayTotal = computed(() => {
|
||||||
break
|
workday.value = props.check(workday.value, 1, 31)
|
||||||
case 6:
|
return workday.value + 'W'
|
||||||
this.$emit('update', 'day', 'L')
|
})
|
||||||
break
|
const checkboxString = computed(() => {
|
||||||
case 7:
|
return checkboxList.value.join(',')
|
||||||
this.$emit('update', 'day', this.checkboxString)
|
})
|
||||||
break
|
watch(() => props.cron.day, value => changeRadioValue(value))
|
||||||
}
|
watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
|
||||||
('day rachange end')
|
function changeRadioValue(value) {
|
||||||
},
|
if (value === "*") {
|
||||||
// 周期两个值变化时
|
radioValue.value = 1
|
||||||
cycleChange() {
|
} else if (value === "?") {
|
||||||
if (this.radioValue == '3') {
|
radioValue.value = 2
|
||||||
this.$emit('update', 'day', this.cycleTotal)
|
} else if (value.indexOf("-") > -1) {
|
||||||
}
|
const indexArr = value.split('-')
|
||||||
},
|
cycle01.value = Number(indexArr[0])
|
||||||
// 平均两个值变化时
|
cycle02.value = Number(indexArr[1])
|
||||||
averageChange() {
|
radioValue.value = 3
|
||||||
if (this.radioValue == '4') {
|
} else if (value.indexOf("/") > -1) {
|
||||||
this.$emit('update', 'day', this.averageTotal)
|
const indexArr = value.split('/')
|
||||||
}
|
average01.value = Number(indexArr[0])
|
||||||
},
|
average02.value = Number(indexArr[1])
|
||||||
// 最近工作日值变化时
|
radioValue.value = 4
|
||||||
workdayChange() {
|
} else if (value.indexOf("W") > -1) {
|
||||||
if (this.radioValue == '5') {
|
const indexArr = value.split("W")
|
||||||
this.$emit('update', 'day', this.workdayCheck + 'W')
|
workday.value = Number(indexArr[0])
|
||||||
}
|
radioValue.value = 5
|
||||||
},
|
} else if (value === "L") {
|
||||||
// checkbox值变化时
|
radioValue.value = 6
|
||||||
checkboxChange() {
|
} else {
|
||||||
if (this.radioValue == '7') {
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
this.$emit('update', 'day', this.checkboxString)
|
radioValue.value = 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
// 单选按钮值变化时
|
||||||
watch: {
|
function onRadioChange() {
|
||||||
'radioValue': 'radioChange',
|
if (radioValue.value === 2 && props.cron.week === '?') {
|
||||||
'cycleTotal': 'cycleChange',
|
emit('update', 'week', '*', 'day')
|
||||||
'averageTotal': 'averageChange',
|
}
|
||||||
'workdayCheck': 'workdayChange',
|
if (radioValue.value !== 2 && props.cron.week !== '?') {
|
||||||
'checkboxString': 'checkboxChange',
|
emit('update', 'week', '?', 'day')
|
||||||
},
|
}
|
||||||
computed: {
|
switch (radioValue.value) {
|
||||||
// 计算两个周期值
|
case 1:
|
||||||
cycleTotal: function () {
|
emit('update', 'day', '*', 'day')
|
||||||
const cycle01 = this.checkNum(this.cycle01, 1, 30)
|
break
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 31, 31)
|
case 2:
|
||||||
return cycle01 + '-' + cycle02
|
emit('update', 'day', '?', 'day')
|
||||||
},
|
break
|
||||||
// 计算平均用到的值
|
case 3:
|
||||||
averageTotal: function () {
|
emit('update', 'day', cycleTotal.value, 'day')
|
||||||
const average01 = this.checkNum(this.average01, 1, 30)
|
break
|
||||||
const average02 = this.checkNum(this.average02, 1, 31 - average01 || 0)
|
case 4:
|
||||||
return average01 + '/' + average02
|
emit('update', 'day', averageTotal.value, 'day')
|
||||||
},
|
break
|
||||||
// 计算工作日格式
|
case 5:
|
||||||
workdayCheck: function () {
|
emit('update', 'day', workdayTotal.value, 'day')
|
||||||
const workday = this.checkNum(this.workday, 1, 31)
|
break
|
||||||
return workday
|
case 6:
|
||||||
},
|
emit('update', 'day', 'L', 'day')
|
||||||
// 计算勾选的checkbox值合集
|
break
|
||||||
checkboxString: function () {
|
case 7:
|
||||||
let str = this.checkboxList.join()
|
if (checkboxList.value.length === 0) {
|
||||||
return str == '' ? '*' : str
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
}
|
} else {
|
||||||
}
|
checkCopy.value = checkboxList.value
|
||||||
|
}
|
||||||
|
emit('update', 'day', checkboxString.value, 'day')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 18.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,120 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size="small">
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
小时,允许的通配符[, - * /]
|
小时,允许的通配符[, - * /]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min="0" :max="22" /> -
|
<el-input-number v-model='cycle01' :min="0" :max="22" /> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> 小时
|
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" /> 时
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min="0" :max="22" /> 小时开始,每
|
<el-input-number v-model='average01' :min="0" :max="22" /> 时开始,每
|
||||||
<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
|
<el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="4">
|
<el-radio v-model='radioValue' :value="4">
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
|
||||||
<el-option v-for="item in 24" :key="item" :value="item-1">{{item-1}}</el-option>
|
<el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
cron: {
|
||||||
radioValue: 1,
|
type: Object,
|
||||||
cycle01: 0,
|
default: {
|
||||||
cycle02: 1,
|
second: "*",
|
||||||
average01: 0,
|
min: "*",
|
||||||
average02: 1,
|
hour: "*",
|
||||||
checkboxList: [],
|
day: "*",
|
||||||
checkNum: this.$options.propsData.check
|
month: "*",
|
||||||
}
|
week: "?",
|
||||||
},
|
year: "",
|
||||||
name: 'crontab-hour',
|
}
|
||||||
props: ['check', 'cron'],
|
},
|
||||||
methods: {
|
check: {
|
||||||
// 单选按钮值变化时
|
type: Function,
|
||||||
radioChange() {
|
default: () => {
|
||||||
if (this.cron.min === '*') {
|
}
|
||||||
this.$emit('update', 'min', '0', 'hour')
|
}
|
||||||
}
|
})
|
||||||
if (this.cron.second === '*') {
|
const radioValue = ref(1)
|
||||||
this.$emit('update', 'second', '0', 'hour')
|
const cycle01 = ref(0)
|
||||||
}
|
const cycle02 = ref(1)
|
||||||
switch (this.radioValue) {
|
const average01 = ref(0)
|
||||||
case 1:
|
const average02 = ref(1)
|
||||||
this.$emit('update', 'hour', '*')
|
const checkboxList = ref([])
|
||||||
break
|
const checkCopy = ref([0])
|
||||||
case 2:
|
const cycleTotal = computed(() => {
|
||||||
this.$emit('update', 'hour', this.cycleTotal)
|
cycle01.value = props.check(cycle01.value, 0, 22)
|
||||||
break
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
|
||||||
case 3:
|
return cycle01.value + '-' + cycle02.value
|
||||||
this.$emit('update', 'hour', this.averageTotal)
|
})
|
||||||
break
|
const averageTotal = computed(() => {
|
||||||
case 4:
|
average01.value = props.check(average01.value, 0, 22)
|
||||||
this.$emit('update', 'hour', this.checkboxString)
|
average02.value = props.check(average02.value, 1, 23 - average01.value)
|
||||||
break
|
return average01.value + '/' + average02.value
|
||||||
}
|
})
|
||||||
},
|
const checkboxString = computed(() => {
|
||||||
// 周期两个值变化时
|
return checkboxList.value.join(',')
|
||||||
cycleChange() {
|
})
|
||||||
if (this.radioValue == '2') {
|
watch(() => props.cron.hour, value => changeRadioValue(value))
|
||||||
this.$emit('update', 'hour', this.cycleTotal)
|
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||||
}
|
function changeRadioValue(value) {
|
||||||
},
|
if (props.cron.min === '*') {
|
||||||
// 平均两个值变化时
|
emit('update', 'min', '0', 'hour')
|
||||||
averageChange() {
|
}
|
||||||
if (this.radioValue == '3') {
|
if (props.cron.second === '*') {
|
||||||
this.$emit('update', 'hour', this.averageTotal)
|
emit('update', 'second', '0', 'hour')
|
||||||
}
|
}
|
||||||
},
|
if (value === '*') {
|
||||||
// checkbox值变化时
|
radioValue.value = 1
|
||||||
checkboxChange() {
|
} else if (value.indexOf('-') > -1) {
|
||||||
if (this.radioValue == '4') {
|
const indexArr = value.split('-')
|
||||||
this.$emit('update', 'hour', this.checkboxString)
|
cycle01.value = Number(indexArr[0])
|
||||||
}
|
cycle02.value = Number(indexArr[1])
|
||||||
}
|
radioValue.value = 2
|
||||||
},
|
} else if (value.indexOf('/') > -1) {
|
||||||
watch: {
|
const indexArr = value.split('/')
|
||||||
'radioValue': 'radioChange',
|
average01.value = Number(indexArr[0])
|
||||||
'cycleTotal': 'cycleChange',
|
average02.value = Number(indexArr[1])
|
||||||
'averageTotal': 'averageChange',
|
radioValue.value = 3
|
||||||
'checkboxString': 'checkboxChange'
|
} else {
|
||||||
},
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
computed: {
|
radioValue.value = 4
|
||||||
// 计算两个周期值
|
}
|
||||||
cycleTotal: function () {
|
}
|
||||||
const cycle01 = this.checkNum(this.cycle01, 0, 22)
|
function onRadioChange() {
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
|
switch (radioValue.value) {
|
||||||
return cycle01 + '-' + cycle02
|
case 1:
|
||||||
},
|
emit('update', 'hour', '*', 'hour')
|
||||||
// 计算平均用到的值
|
break
|
||||||
averageTotal: function () {
|
case 2:
|
||||||
const average01 = this.checkNum(this.average01, 0, 22)
|
emit('update', 'hour', cycleTotal.value, 'hour')
|
||||||
const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
|
break
|
||||||
return average01 + '/' + average02
|
case 3:
|
||||||
},
|
emit('update', 'hour', averageTotal.value, 'hour')
|
||||||
// 计算勾选的checkbox值合集
|
break
|
||||||
checkboxString: function () {
|
case 4:
|
||||||
let str = this.checkboxList.join()
|
if (checkboxList.value.length === 0) {
|
||||||
return str == '' ? '*' : str
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
}
|
} else {
|
||||||
}
|
checkCopy.value = checkboxList.value
|
||||||
|
}
|
||||||
|
emit('update', 'hour', checkboxString.value, 'hour')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 18.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,118 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-tabs type="border-card">
|
<el-tabs type="border-card">
|
||||||
<el-tab-pane label="秒" v-if="shouldHide('second')">
|
<el-tab-pane label="秒" v-if="shouldHide('second')">
|
||||||
<CrontabSecond
|
<CrontabSecond
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronsecond"
|
ref="cronsecond"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="分钟" v-if="shouldHide('min')">
|
<el-tab-pane label="分钟" v-if="shouldHide('min')">
|
||||||
<CrontabMin
|
<CrontabMin
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronmin"
|
ref="cronmin"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="小时" v-if="shouldHide('hour')">
|
<el-tab-pane label="小时" v-if="shouldHide('hour')">
|
||||||
<CrontabHour
|
<CrontabHour
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronhour"
|
ref="cronhour"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="日" v-if="shouldHide('day')">
|
<el-tab-pane label="日" v-if="shouldHide('day')">
|
||||||
<CrontabDay
|
<CrontabDay
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronday"
|
ref="cronday"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="月" v-if="shouldHide('month')">
|
<el-tab-pane label="月" v-if="shouldHide('month')">
|
||||||
<CrontabMonth
|
<CrontabMonth
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronmonth"
|
ref="cronmonth"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="周" v-if="shouldHide('week')">
|
<el-tab-pane label="周" v-if="shouldHide('week')">
|
||||||
<CrontabWeek
|
<CrontabWeek
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronweek"
|
ref="cronweek"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="年" v-if="shouldHide('year')">
|
<el-tab-pane label="年" v-if="shouldHide('year')">
|
||||||
<CrontabYear
|
<CrontabYear
|
||||||
@update="updateCrontabValue"
|
@update="updateCrontabValue"
|
||||||
:check="checkNumber"
|
:check="checkNumber"
|
||||||
:cron="crontabValueObj"
|
:cron="crontabValueObj"
|
||||||
ref="cronyear"
|
ref="cronyear"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<div class="popup-main">
|
<div class="popup-main">
|
||||||
<div class="popup-result">
|
<div class="popup-result">
|
||||||
<p class="title">时间表达式</p>
|
<p class="title">时间表达式</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<th v-for="item of tabTitles" width="40" :key="item">{{item}}</th>
|
<tr>
|
||||||
<th>Cron 表达式</th>
|
<th v-for="item of tabTitles" :key="item">{{item}}</th>
|
||||||
</thead>
|
<th>Cron 表达式</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<span>{{crontabValueObj.second}}</span>
|
<tbody>
|
||||||
</td>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{crontabValueObj.min}}</span>
|
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
|
||||||
</td>
|
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
|
||||||
<td>
|
</td>
|
||||||
<span>{{crontabValueObj.hour}}</span>
|
<td>
|
||||||
</td>
|
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
|
||||||
<td>
|
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
|
||||||
<span>{{crontabValueObj.day}}</span>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
|
||||||
<span>{{crontabValueObj.month}}</span>
|
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>{{crontabValueObj.week}}</span>
|
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
|
||||||
</td>
|
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
|
||||||
<td>
|
</td>
|
||||||
<span>{{crontabValueObj.year}}</span>
|
<td>
|
||||||
</td>
|
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
|
||||||
<td>
|
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
|
||||||
<span>{{crontabValueString}}</span>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
</tbody>
|
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
|
||||||
</table>
|
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
|
||||||
</div>
|
</td>
|
||||||
<CrontabResult :ex="crontabValueString"></CrontabResult>
|
<td>
|
||||||
|
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
|
||||||
|
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
|
||||||
|
</td>
|
||||||
|
<td class="result">
|
||||||
|
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
|
||||||
|
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<CrontabResult :ex="crontabValueString"></CrontabResult>
|
||||||
|
|
||||||
<div class="pop_btn">
|
<div class="pop_btn">
|
||||||
<el-button size="small" type="primary" @click="submitFill">确定</el-button>
|
<el-button type="primary" @click="submitFill">确定</el-button>
|
||||||
<el-button size="small" type="warning" @click="clearCron">重置</el-button>
|
<el-button type="warning" @click="clearCron">重置</el-button>
|
||||||
<el-button size="small" @click="hidePopup">取消</el-button>
|
<el-button @click="hidePopup">取消</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import CrontabSecond from "./second.vue"
|
import CrontabSecond from "./second.vue"
|
||||||
import CrontabMin from "./min.vue"
|
import CrontabMin from "./min.vue"
|
||||||
import CrontabHour from "./hour.vue"
|
import CrontabHour from "./hour.vue"
|
||||||
@@ -121,200 +133,105 @@ import CrontabMonth from "./month.vue"
|
|||||||
import CrontabWeek from "./week.vue"
|
import CrontabWeek from "./week.vue"
|
||||||
import CrontabYear from "./year.vue"
|
import CrontabYear from "./year.vue"
|
||||||
import CrontabResult from "./result.vue"
|
import CrontabResult from "./result.vue"
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
export default {
|
const emit = defineEmits(['hide', 'fill'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
hideComponent: {
|
||||||
tabTitles: ["秒", "分钟", "小时", "日", "月", "周", "年"],
|
type: Array,
|
||||||
tabActive: 0,
|
default: () => [],
|
||||||
myindex: 0,
|
},
|
||||||
crontabValueObj: {
|
expression: {
|
||||||
second: "*",
|
type: String,
|
||||||
min: "*",
|
default: ""
|
||||||
hour: "*",
|
|
||||||
day: "*",
|
|
||||||
month: "*",
|
|
||||||
week: "?",
|
|
||||||
year: "",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
name: "vcrontab",
|
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
|
||||||
props: ["expression", "hideComponent"],
|
const tabActive = ref(0)
|
||||||
methods: {
|
const hideComponent = ref([])
|
||||||
shouldHide(key) {
|
const expression = ref('')
|
||||||
if (this.hideComponent && this.hideComponent.includes(key)) return false
|
const crontabValueObj = ref({
|
||||||
return true
|
second: "*",
|
||||||
},
|
min: "*",
|
||||||
resolveExp() {
|
hour: "*",
|
||||||
// 反解析 表达式
|
day: "*",
|
||||||
if (this.expression) {
|
month: "*",
|
||||||
let arr = this.expression.split(" ")
|
week: "?",
|
||||||
|
year: "",
|
||||||
|
})
|
||||||
|
const crontabValueString = computed(() => {
|
||||||
|
const obj = crontabValueObj.value
|
||||||
|
return obj.second
|
||||||
|
+ " "
|
||||||
|
+ obj.min
|
||||||
|
+ " "
|
||||||
|
+ obj.hour
|
||||||
|
+ " "
|
||||||
|
+ obj.day
|
||||||
|
+ " "
|
||||||
|
+ obj.month
|
||||||
|
+ " "
|
||||||
|
+ obj.week
|
||||||
|
+ (obj.year === "" ? "" : " " + obj.year)
|
||||||
|
})
|
||||||
|
watch(expression, () => resolveExp())
|
||||||
|
function shouldHide(key) {
|
||||||
|
return !(hideComponent.value && hideComponent.value.includes(key))
|
||||||
|
}
|
||||||
|
function resolveExp() {
|
||||||
|
// 反解析 表达式
|
||||||
|
if (expression.value) {
|
||||||
|
const arr = expression.value.split(/\s+/)
|
||||||
if (arr.length >= 6) {
|
if (arr.length >= 6) {
|
||||||
//6 位以上是合法表达式
|
//6 位以上是合法表达式
|
||||||
let obj = {
|
let obj = {
|
||||||
second: arr[0],
|
second: arr[0],
|
||||||
min: arr[1],
|
min: arr[1],
|
||||||
hour: arr[2],
|
hour: arr[2],
|
||||||
day: arr[3],
|
day: arr[3],
|
||||||
month: arr[4],
|
month: arr[4],
|
||||||
week: arr[5],
|
week: arr[5],
|
||||||
year: arr[6] ? arr[6] : "",
|
year: arr[6] ? arr[6] : ""
|
||||||
}
|
}
|
||||||
this.crontabValueObj = {
|
crontabValueObj.value = {
|
||||||
...obj,
|
...obj,
|
||||||
}
|
}
|
||||||
for (let i in obj) {
|
|
||||||
if (obj[i]) this.changeRadio(i, obj[i])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有传入的表达式 则还原
|
// 没有传入的表达式 则还原
|
||||||
this.clearCron()
|
clearCron()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// tab切换值
|
// tab切换值
|
||||||
tabCheck(index) {
|
function tabCheck(index) {
|
||||||
this.tabActive = index
|
tabActive.value = index
|
||||||
},
|
}
|
||||||
// 由子组件触发,更改表达式组成的字段值
|
// 由子组件触发,更改表达式组成的字段值
|
||||||
updateCrontabValue(name, value, from) {
|
function updateCrontabValue(name, value, from) {
|
||||||
"updateCrontabValue", name, value, from
|
crontabValueObj.value[name] = value
|
||||||
this.crontabValueObj[name] = value
|
}
|
||||||
if (from && from !== name) {
|
// 表单选项的子组件校验数字格式(通过-props传递)
|
||||||
console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
|
function checkNumber(value, minLimit, maxLimit) {
|
||||||
this.changeRadio(name, value)
|
// 检查必须为整数
|
||||||
}
|
value = Math.floor(value)
|
||||||
},
|
if (value < minLimit) {
|
||||||
// 赋值到组件
|
|
||||||
changeRadio(name, value) {
|
|
||||||
let arr = ["second", "min", "hour", "month"],
|
|
||||||
refName = "cron" + name,
|
|
||||||
insValue
|
|
||||||
|
|
||||||
if (!this.$refs[refName]) return
|
|
||||||
|
|
||||||
if (arr.includes(name)) {
|
|
||||||
if (value === "*") {
|
|
||||||
insValue = 1
|
|
||||||
} else if (value.indexOf("-") > -1) {
|
|
||||||
let indexArr = value.split("-")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].cycle01 = 0)
|
|
||||||
: (this.$refs[refName].cycle01 = indexArr[0])
|
|
||||||
this.$refs[refName].cycle02 = indexArr[1]
|
|
||||||
insValue = 2
|
|
||||||
} else if (value.indexOf("/") > -1) {
|
|
||||||
let indexArr = value.split("/")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].average01 = 0)
|
|
||||||
: (this.$refs[refName].average01 = indexArr[0])
|
|
||||||
this.$refs[refName].average02 = indexArr[1]
|
|
||||||
insValue = 3
|
|
||||||
} else {
|
|
||||||
insValue = 4
|
|
||||||
this.$refs[refName].checkboxList = value.split(",")
|
|
||||||
}
|
|
||||||
} else if (name == "day") {
|
|
||||||
if (value === "*") {
|
|
||||||
insValue = 1
|
|
||||||
} else if (value == "?") {
|
|
||||||
insValue = 2
|
|
||||||
} else if (value.indexOf("-") > -1) {
|
|
||||||
let indexArr = value.split("-")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].cycle01 = 0)
|
|
||||||
: (this.$refs[refName].cycle01 = indexArr[0])
|
|
||||||
this.$refs[refName].cycle02 = indexArr[1]
|
|
||||||
insValue = 3
|
|
||||||
} else if (value.indexOf("/") > -1) {
|
|
||||||
let indexArr = value.split("/")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].average01 = 0)
|
|
||||||
: (this.$refs[refName].average01 = indexArr[0])
|
|
||||||
this.$refs[refName].average02 = indexArr[1]
|
|
||||||
insValue = 4
|
|
||||||
} else if (value.indexOf("W") > -1) {
|
|
||||||
let indexArr = value.split("W")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].workday = 0)
|
|
||||||
: (this.$refs[refName].workday = indexArr[0])
|
|
||||||
insValue = 5
|
|
||||||
} else if (value === "L") {
|
|
||||||
insValue = 6
|
|
||||||
} else {
|
|
||||||
this.$refs[refName].checkboxList = value.split(",")
|
|
||||||
insValue = 7
|
|
||||||
}
|
|
||||||
} else if (name == "week") {
|
|
||||||
if (value === "*") {
|
|
||||||
insValue = 1
|
|
||||||
} else if (value == "?") {
|
|
||||||
insValue = 2
|
|
||||||
} else if (value.indexOf("-") > -1) {
|
|
||||||
let indexArr = value.split("-")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].cycle01 = 0)
|
|
||||||
: (this.$refs[refName].cycle01 = indexArr[0])
|
|
||||||
this.$refs[refName].cycle02 = indexArr[1]
|
|
||||||
insValue = 3
|
|
||||||
} else if (value.indexOf("#") > -1) {
|
|
||||||
let indexArr = value.split("#")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].average01 = 1)
|
|
||||||
: (this.$refs[refName].average01 = indexArr[0])
|
|
||||||
this.$refs[refName].average02 = indexArr[1]
|
|
||||||
insValue = 4
|
|
||||||
} else if (value.indexOf("L") > -1) {
|
|
||||||
let indexArr = value.split("L")
|
|
||||||
isNaN(indexArr[0])
|
|
||||||
? (this.$refs[refName].weekday = 1)
|
|
||||||
: (this.$refs[refName].weekday = indexArr[0])
|
|
||||||
insValue = 5
|
|
||||||
} else {
|
|
||||||
this.$refs[refName].checkboxList = value.split(",")
|
|
||||||
insValue = 6
|
|
||||||
}
|
|
||||||
} else if (name == "year") {
|
|
||||||
if (value == "") {
|
|
||||||
insValue = 1
|
|
||||||
} else if (value == "*") {
|
|
||||||
insValue = 2
|
|
||||||
} else if (value.indexOf("-") > -1) {
|
|
||||||
insValue = 3
|
|
||||||
} else if (value.indexOf("/") > -1) {
|
|
||||||
insValue = 4
|
|
||||||
} else {
|
|
||||||
this.$refs[refName].checkboxList = value.split(",")
|
|
||||||
insValue = 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$refs[refName].radioValue = insValue
|
|
||||||
},
|
|
||||||
// 表单选项的子组件校验数字格式(通过-props传递)
|
|
||||||
checkNumber(value, minLimit, maxLimit) {
|
|
||||||
// 检查必须为整数
|
|
||||||
value = Math.floor(value)
|
|
||||||
if (value < minLimit) {
|
|
||||||
value = minLimit
|
value = minLimit
|
||||||
} else if (value > maxLimit) {
|
} else if (value > maxLimit) {
|
||||||
value = maxLimit
|
value = maxLimit
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
},
|
}
|
||||||
// 隐藏弹窗
|
// 隐藏弹窗
|
||||||
hidePopup() {
|
function hidePopup() {
|
||||||
this.$emit("hide")
|
emit("hide")
|
||||||
},
|
}
|
||||||
// 填充表达式
|
// 填充表达式
|
||||||
submitFill() {
|
function submitFill() {
|
||||||
this.$emit("fill", this.crontabValueString)
|
emit("fill", crontabValueString.value)
|
||||||
this.hidePopup()
|
hidePopup()
|
||||||
},
|
}
|
||||||
clearCron() {
|
function clearCron() {
|
||||||
// 还原选择项
|
// 还原选择项
|
||||||
("准备还原")
|
crontabValueObj.value = {
|
||||||
this.crontabValueObj = {
|
|
||||||
second: "*",
|
second: "*",
|
||||||
min: "*",
|
min: "*",
|
||||||
hour: "*",
|
hour: "*",
|
||||||
@@ -322,109 +239,75 @@ export default {
|
|||||||
month: "*",
|
month: "*",
|
||||||
week: "?",
|
week: "?",
|
||||||
year: "",
|
year: "",
|
||||||
}
|
}
|
||||||
for (let j in this.crontabValueObj) {
|
|
||||||
this.changeRadio(j, this.crontabValueObj[j])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
crontabValueString: function() {
|
|
||||||
let obj = this.crontabValueObj
|
|
||||||
let str =
|
|
||||||
obj.second +
|
|
||||||
" " +
|
|
||||||
obj.min +
|
|
||||||
" " +
|
|
||||||
obj.hour +
|
|
||||||
" " +
|
|
||||||
obj.day +
|
|
||||||
" " +
|
|
||||||
obj.month +
|
|
||||||
" " +
|
|
||||||
obj.week +
|
|
||||||
(obj.year == "" ? "" : " " + obj.year)
|
|
||||||
return str
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CrontabSecond,
|
|
||||||
CrontabMin,
|
|
||||||
CrontabHour,
|
|
||||||
CrontabDay,
|
|
||||||
CrontabMonth,
|
|
||||||
CrontabWeek,
|
|
||||||
CrontabYear,
|
|
||||||
CrontabResult,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
expression: "resolveExp",
|
|
||||||
hideComponent(value) {
|
|
||||||
// 隐藏部分组件
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted: function() {
|
|
||||||
this.resolveExp()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
expression.value = props.expression
|
||||||
|
hideComponent.value = props.hideComponent
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
.pop_btn {
|
.pop_btn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.popup-main {
|
.popup-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
background: #fff;
|
border-radius: 5px;
|
||||||
border-radius: 5px;
|
font-size: 12px;
|
||||||
font-size: 12px;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.popup-title {
|
.popup-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
}
|
}
|
||||||
.popup-result {
|
.popup-result {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
margin: 25px auto;
|
margin: 25px auto;
|
||||||
padding: 15px 10px 10px;
|
padding: 15px 10px 10px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.popup-result .title {
|
.popup-result .title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -28px;
|
top: -28px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-left: -70px;
|
margin-left: -70px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popup-result table {
|
.popup-result table {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.popup-result table td:not(.result) {
|
||||||
|
width: 3.5rem;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
max-width: 3.5rem;
|
||||||
}
|
}
|
||||||
.popup-result table span {
|
.popup-result table span {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: arial;
|
font-family: arial;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
}
|
}
|
||||||
.popup-result-scroll {
|
.popup-result-scroll {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
height: 10em;
|
height: 10em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,116 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size="small">
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
分钟,允许的通配符[, - * /]
|
分钟,允许的通配符[, - * /]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
|
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟
|
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始,每
|
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始, 每
|
||||||
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 分钟执行一次
|
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-radio v-model='radioValue' :label="4">
|
|
||||||
指定
|
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
|
||||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-radio>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-radio v-model='radioValue' :value="4">
|
||||||
|
指定
|
||||||
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
|
||||||
|
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
|
||||||
|
</el-select>
|
||||||
|
</el-radio>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script>
|
const emit = defineEmits(['update'])
|
||||||
export default {
|
const props = defineProps({
|
||||||
data() {
|
cron: {
|
||||||
return {
|
type: Object,
|
||||||
radioValue: 1,
|
default: {
|
||||||
cycle01: 1,
|
second: "*",
|
||||||
cycle02: 2,
|
min: "*",
|
||||||
average01: 0,
|
hour: "*",
|
||||||
average02: 1,
|
day: "*",
|
||||||
checkboxList: [],
|
month: "*",
|
||||||
checkNum: this.$options.propsData.check
|
week: "?",
|
||||||
}
|
year: "",
|
||||||
},
|
}
|
||||||
name: 'crontab-min',
|
},
|
||||||
props: ['check', 'cron'],
|
check: {
|
||||||
methods: {
|
type: Function,
|
||||||
// 单选按钮值变化时
|
default: () => {
|
||||||
radioChange() {
|
}
|
||||||
switch (this.radioValue) {
|
}
|
||||||
case 1:
|
})
|
||||||
this.$emit('update', 'min', '*', 'min')
|
const radioValue = ref(1)
|
||||||
break
|
const cycle01 = ref(0)
|
||||||
case 2:
|
const cycle02 = ref(1)
|
||||||
this.$emit('update', 'min', this.cycleTotal, 'min')
|
const average01 = ref(0)
|
||||||
break
|
const average02 = ref(1)
|
||||||
case 3:
|
const checkboxList = ref([])
|
||||||
this.$emit('update', 'min', this.averageTotal, 'min')
|
const checkCopy = ref([0])
|
||||||
break
|
const cycleTotal = computed(() => {
|
||||||
case 4:
|
cycle01.value = props.check(cycle01.value, 0, 58)
|
||||||
this.$emit('update', 'min', this.checkboxString, 'min')
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
|
||||||
break
|
return cycle01.value + '-' + cycle02.value
|
||||||
}
|
})
|
||||||
},
|
const averageTotal = computed(() => {
|
||||||
// 周期两个值变化时
|
average01.value = props.check(average01.value, 0, 58)
|
||||||
cycleChange() {
|
average02.value = props.check(average02.value, 1, 59 - average01.value)
|
||||||
if (this.radioValue == '2') {
|
return average01.value + '/' + average02.value
|
||||||
this.$emit('update', 'min', this.cycleTotal, 'min')
|
})
|
||||||
}
|
const checkboxString = computed(() => {
|
||||||
},
|
return checkboxList.value.join(',')
|
||||||
// 平均两个值变化时
|
})
|
||||||
averageChange() {
|
watch(() => props.cron.min, value => changeRadioValue(value))
|
||||||
if (this.radioValue == '3') {
|
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||||
this.$emit('update', 'min', this.averageTotal, 'min')
|
function changeRadioValue(value) {
|
||||||
}
|
if (value === '*') {
|
||||||
},
|
radioValue.value = 1
|
||||||
// checkbox值变化时
|
} else if (value.indexOf('-') > -1) {
|
||||||
checkboxChange() {
|
const indexArr = value.split('-')
|
||||||
if (this.radioValue == '4') {
|
cycle01.value = Number(indexArr[0])
|
||||||
this.$emit('update', 'min', this.checkboxString, 'min')
|
cycle02.value = Number(indexArr[1])
|
||||||
}
|
radioValue.value = 2
|
||||||
},
|
} else if (value.indexOf('/') > -1) {
|
||||||
|
const indexArr = value.split('/')
|
||||||
},
|
average01.value = Number(indexArr[0])
|
||||||
watch: {
|
average02.value = Number(indexArr[1])
|
||||||
'radioValue': 'radioChange',
|
radioValue.value = 3
|
||||||
'cycleTotal': 'cycleChange',
|
} else {
|
||||||
'averageTotal': 'averageChange',
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
'checkboxString': 'checkboxChange',
|
radioValue.value = 4
|
||||||
},
|
}
|
||||||
computed: {
|
|
||||||
// 计算两个周期值
|
|
||||||
cycleTotal: function () {
|
|
||||||
const cycle01 = this.checkNum(this.cycle01, 0, 58)
|
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
|
|
||||||
return cycle01 + '-' + cycle02
|
|
||||||
},
|
|
||||||
// 计算平均用到的值
|
|
||||||
averageTotal: function () {
|
|
||||||
const average01 = this.checkNum(this.average01, 0, 58)
|
|
||||||
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
|
|
||||||
return average01 + '/' + average02
|
|
||||||
},
|
|
||||||
// 计算勾选的checkbox值合集
|
|
||||||
checkboxString: function () {
|
|
||||||
let str = this.checkboxList.join()
|
|
||||||
return str == '' ? '*' : str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
function onRadioChange() {
|
||||||
|
switch (radioValue.value) {
|
||||||
|
case 1:
|
||||||
|
emit('update', 'min', '*', 'min')
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
emit('update', 'min', cycleTotal.value, 'min')
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
emit('update', 'min', averageTotal.value, 'min')
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
if (checkboxList.value.length === 0) {
|
||||||
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
|
} else {
|
||||||
|
checkCopy.value = checkboxList.value
|
||||||
|
}
|
||||||
|
emit('update', 'min', checkboxString.value, 'min')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 19.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,114 +1,141 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size='small'>
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
月,允许的通配符[, - * /]
|
月,允许的通配符[, - * /]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min="1" :max="11" /> -
|
<el-input-number v-model='cycle01' :min="1" :max="11" /> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="12" /> 月
|
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" /> 月
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min="1" :max="11" /> 月开始,每
|
<el-input-number v-model='average01' :min="1" :max="11" /> 月开始,每
|
||||||
<el-input-number v-model='average02' :min="1" :max="12 - average01 || 0" /> 月月执行一次
|
<el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="4">
|
<el-radio v-model='radioValue' :value="4">
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
|
||||||
<el-option v-for="item in 12" :key="item" :value="item">{{item}}</el-option>
|
<el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
cron: {
|
||||||
radioValue: 1,
|
type: Object,
|
||||||
cycle01: 1,
|
default: {
|
||||||
cycle02: 2,
|
second: "*",
|
||||||
average01: 1,
|
min: "*",
|
||||||
average02: 1,
|
hour: "*",
|
||||||
checkboxList: [],
|
day: "*",
|
||||||
checkNum: this.check
|
month: "*",
|
||||||
}
|
week: "?",
|
||||||
},
|
year: "",
|
||||||
name: 'crontab-month',
|
}
|
||||||
props: ['check', 'cron'],
|
},
|
||||||
methods: {
|
check: {
|
||||||
// 单选按钮值变化时
|
type: Function,
|
||||||
radioChange() {
|
default: () => {
|
||||||
switch (this.radioValue) {
|
}
|
||||||
case 1:
|
}
|
||||||
this.$emit('update', 'month', '*')
|
})
|
||||||
break
|
const radioValue = ref(1)
|
||||||
case 2:
|
const cycle01 = ref(1)
|
||||||
this.$emit('update', 'month', this.cycleTotal)
|
const cycle02 = ref(2)
|
||||||
break
|
const average01 = ref(1)
|
||||||
case 3:
|
const average02 = ref(1)
|
||||||
this.$emit('update', 'month', this.averageTotal)
|
const checkboxList = ref([])
|
||||||
break
|
const checkCopy = ref([1])
|
||||||
case 4:
|
const monthList = ref([
|
||||||
this.$emit('update', 'month', this.checkboxString)
|
{key: 1, value: '一月'},
|
||||||
break
|
{key: 2, value: '二月'},
|
||||||
}
|
{key: 3, value: '三月'},
|
||||||
},
|
{key: 4, value: '四月'},
|
||||||
// 周期两个值变化时
|
{key: 5, value: '五月'},
|
||||||
cycleChange() {
|
{key: 6, value: '六月'},
|
||||||
if (this.radioValue == '2') {
|
{key: 7, value: '七月'},
|
||||||
this.$emit('update', 'month', this.cycleTotal)
|
{key: 8, value: '八月'},
|
||||||
}
|
{key: 9, value: '九月'},
|
||||||
},
|
{key: 10, value: '十月'},
|
||||||
// 平均两个值变化时
|
{key: 11, value: '十一月'},
|
||||||
averageChange() {
|
{key: 12, value: '十二月'}
|
||||||
if (this.radioValue == '3') {
|
])
|
||||||
this.$emit('update', 'month', this.averageTotal)
|
const cycleTotal = computed(() => {
|
||||||
}
|
cycle01.value = props.check(cycle01.value, 1, 11)
|
||||||
},
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
|
||||||
// checkbox值变化时
|
return cycle01.value + '-' + cycle02.value
|
||||||
checkboxChange() {
|
})
|
||||||
if (this.radioValue == '4') {
|
const averageTotal = computed(() => {
|
||||||
this.$emit('update', 'month', this.checkboxString)
|
average01.value = props.check(average01.value, 1, 11)
|
||||||
}
|
average02.value = props.check(average02.value, 1, 12 - average01.value)
|
||||||
}
|
return average01.value + '/' + average02.value
|
||||||
},
|
})
|
||||||
watch: {
|
const checkboxString = computed(() => {
|
||||||
'radioValue': 'radioChange',
|
return checkboxList.value.join(',')
|
||||||
'cycleTotal': 'cycleChange',
|
})
|
||||||
'averageTotal': 'averageChange',
|
watch(() => props.cron.month, value => changeRadioValue(value))
|
||||||
'checkboxString': 'checkboxChange'
|
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||||
},
|
function changeRadioValue(value) {
|
||||||
computed: {
|
if (value === '*') {
|
||||||
// 计算两个周期值
|
radioValue.value = 1
|
||||||
cycleTotal: function () {
|
} else if (value.indexOf('-') > -1) {
|
||||||
const cycle01 = this.checkNum(this.cycle01, 1, 11)
|
const indexArr = value.split('-')
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 12)
|
cycle01.value = Number(indexArr[0])
|
||||||
return cycle01 + '-' + cycle02
|
cycle02.value = Number(indexArr[1])
|
||||||
},
|
radioValue.value = 2
|
||||||
// 计算平均用到的值
|
} else if (value.indexOf('/') > -1) {
|
||||||
averageTotal: function () {
|
const indexArr = value.split('/')
|
||||||
const average01 = this.checkNum(this.average01, 1, 11)
|
average01.value = Number(indexArr[0])
|
||||||
const average02 = this.checkNum(this.average02, 1, 12 - average01 || 0)
|
average02.value = Number(indexArr[1])
|
||||||
return average01 + '/' + average02
|
radioValue.value = 3
|
||||||
},
|
} else {
|
||||||
// 计算勾选的checkbox值合集
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
checkboxString: function () {
|
radioValue.value = 4
|
||||||
let str = this.checkboxList.join()
|
}
|
||||||
return str == '' ? '*' : str
|
}
|
||||||
}
|
function onRadioChange() {
|
||||||
}
|
switch (radioValue.value) {
|
||||||
|
case 1:
|
||||||
|
emit('update', 'month', '*', 'month')
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
emit('update', 'month', cycleTotal.value, 'month')
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
emit('update', 'month', averageTotal.value, 'month')
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
if (checkboxList.value.length === 0) {
|
||||||
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
|
} else {
|
||||||
|
checkCopy.value = checkboxList.value
|
||||||
|
}
|
||||||
|
emit('update', 'month', checkboxString.value, 'month')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 18.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size="small">
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
秒,允许的通配符[, - * /]
|
秒,允许的通配符[, - * /]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
|
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 秒
|
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 秒
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始,每
|
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始,每
|
||||||
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 秒执行一次
|
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="4">
|
<el-radio v-model='radioValue' :value="4">
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
|
||||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
|
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
cron: {
|
||||||
radioValue: 1,
|
type: Object,
|
||||||
cycle01: 1,
|
default: {
|
||||||
cycle02: 2,
|
second: "*",
|
||||||
average01: 0,
|
min: "*",
|
||||||
average02: 1,
|
hour: "*",
|
||||||
checkboxList: [],
|
day: "*",
|
||||||
checkNum: this.$options.propsData.check
|
month: "*",
|
||||||
}
|
week: "?",
|
||||||
},
|
year: "",
|
||||||
name: 'crontab-second',
|
}
|
||||||
props: ['check', 'radioParent'],
|
},
|
||||||
methods: {
|
check: {
|
||||||
// 单选按钮值变化时
|
type: Function,
|
||||||
radioChange() {
|
default: () => {
|
||||||
switch (this.radioValue) {
|
}
|
||||||
case 1:
|
}
|
||||||
this.$emit('update', 'second', '*', 'second')
|
})
|
||||||
break
|
const radioValue = ref(1)
|
||||||
case 2:
|
const cycle01 = ref(0)
|
||||||
this.$emit('update', 'second', this.cycleTotal)
|
const cycle02 = ref(1)
|
||||||
break
|
const average01 = ref(0)
|
||||||
case 3:
|
const average02 = ref(1)
|
||||||
this.$emit('update', 'second', this.averageTotal)
|
const checkboxList = ref([])
|
||||||
break
|
const checkCopy = ref([0])
|
||||||
case 4:
|
const cycleTotal = computed(() => {
|
||||||
this.$emit('update', 'second', this.checkboxString)
|
cycle01.value = props.check(cycle01.value, 0, 58)
|
||||||
break
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
|
||||||
}
|
return cycle01.value + '-' + cycle02.value
|
||||||
},
|
})
|
||||||
// 周期两个值变化时
|
const averageTotal = computed(() => {
|
||||||
cycleChange() {
|
average01.value = props.check(average01.value, 0, 58)
|
||||||
if (this.radioValue == '2') {
|
average02.value = props.check(average02.value, 1, 59 - average01.value)
|
||||||
this.$emit('update', 'second', this.cycleTotal)
|
return average01.value + '/' + average02.value
|
||||||
}
|
})
|
||||||
},
|
const checkboxString = computed(() => {
|
||||||
// 平均两个值变化时
|
return checkboxList.value.join(',')
|
||||||
averageChange() {
|
})
|
||||||
if (this.radioValue == '3') {
|
watch(() => props.cron.second, value => changeRadioValue(value))
|
||||||
this.$emit('update', 'second', this.averageTotal)
|
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||||
}
|
function changeRadioValue(value) {
|
||||||
},
|
if (value === '*') {
|
||||||
// checkbox值变化时
|
radioValue.value = 1
|
||||||
checkboxChange() {
|
} else if (value.indexOf('-') > -1) {
|
||||||
if (this.radioValue == '4') {
|
const indexArr = value.split('-')
|
||||||
this.$emit('update', 'second', this.checkboxString)
|
cycle01.value = Number(indexArr[0])
|
||||||
}
|
cycle02.value = Number(indexArr[1])
|
||||||
}
|
radioValue.value = 2
|
||||||
},
|
} else if (value.indexOf('/') > -1) {
|
||||||
watch: {
|
const indexArr = value.split('/')
|
||||||
'radioValue': 'radioChange',
|
average01.value = Number(indexArr[0])
|
||||||
'cycleTotal': 'cycleChange',
|
average02.value = Number(indexArr[1])
|
||||||
'averageTotal': 'averageChange',
|
radioValue.value = 3
|
||||||
'checkboxString': 'checkboxChange',
|
} else {
|
||||||
radioParent() {
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
this.radioValue = this.radioParent
|
radioValue.value = 4
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
computed: {
|
// 单选按钮值变化时
|
||||||
// 计算两个周期值
|
function onRadioChange() {
|
||||||
cycleTotal: function () {
|
switch (radioValue.value) {
|
||||||
const cycle01 = this.checkNum(this.cycle01, 0, 58)
|
case 1:
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
|
emit('update', 'second', '*', 'second')
|
||||||
return cycle01 + '-' + cycle02
|
break
|
||||||
},
|
case 2:
|
||||||
// 计算平均用到的值
|
emit('update', 'second', cycleTotal.value, 'second')
|
||||||
averageTotal: function () {
|
break
|
||||||
const average01 = this.checkNum(this.average01, 0, 58)
|
case 3:
|
||||||
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
|
emit('update', 'second', averageTotal.value, 'second')
|
||||||
return average01 + '/' + average02
|
break
|
||||||
},
|
case 4:
|
||||||
// 计算勾选的checkbox值合集
|
if (checkboxList.value.length === 0) {
|
||||||
checkboxString: function () {
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
let str = this.checkboxList.join()
|
} else {
|
||||||
return str == '' ? '*' : str
|
checkCopy.value = checkboxList.value
|
||||||
}
|
}
|
||||||
}
|
emit('update', 'second', checkboxString.value, 'second')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 18.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,202 +1,197 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size='small'>
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="1">
|
<el-radio v-model='radioValue' :value="1">
|
||||||
周,允许的通配符[, - * ? / L #]
|
周,允许的通配符[, - * ? / L #]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="2">
|
<el-radio v-model='radioValue' :value="2">
|
||||||
不指定
|
不指定
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="3">
|
<el-radio v-model='radioValue' :value="3">
|
||||||
周期从星期
|
周期从
|
||||||
<el-select clearable v-model="cycle01">
|
<el-select clearable v-model="cycle01">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(item,index) of weekList"
|
v-for="(item,index) of weekList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:label="item.value"
|
:label="item.value"
|
||||||
:value="item.key"
|
:value="item.key"
|
||||||
:disabled="item.key === 1"
|
:disabled="item.key === 7"
|
||||||
>{{item.value}}</el-option>
|
>{{item.value}}</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
-
|
-
|
||||||
<el-select clearable v-model="cycle02">
|
<el-select clearable v-model="cycle02">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(item,index) of weekList"
|
v-for="(item,index) of weekList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:label="item.value"
|
:label="item.value"
|
||||||
:value="item.key"
|
:value="item.key"
|
||||||
:disabled="item.key < cycle01 && item.key !== 1"
|
:disabled="item.key <= cycle01"
|
||||||
>{{item.value}}</el-option>
|
>{{item.value}}</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="4">
|
<el-radio v-model='radioValue' :value="4">
|
||||||
第
|
第
|
||||||
<el-input-number v-model='average01' :min="1" :max="4" /> 周的星期
|
<el-input-number v-model='average01' :min="1" :max="4" /> 周的
|
||||||
<el-select clearable v-model="average02">
|
<el-select clearable v-model="average02">
|
||||||
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
|
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="5">
|
<el-radio v-model='radioValue' :value="5">
|
||||||
本月最后一个星期
|
本月最后一个
|
||||||
<el-select clearable v-model="weekday">
|
<el-select clearable v-model="weekday">
|
||||||
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
|
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio v-model='radioValue' :label="6">
|
<el-radio v-model='radioValue' :value="6">
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
|
<el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
|
||||||
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="String(item.key)">{{item.value}}</el-option>
|
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
cron: {
|
||||||
radioValue: 2,
|
type: Object,
|
||||||
weekday: 2,
|
default: {
|
||||||
cycle01: 2,
|
second: "*",
|
||||||
cycle02: 3,
|
min: "*",
|
||||||
average01: 1,
|
hour: "*",
|
||||||
average02: 2,
|
day: "*",
|
||||||
checkboxList: [],
|
month: "*",
|
||||||
weekList: [
|
week: "?",
|
||||||
{
|
year: ""
|
||||||
key: 2,
|
}
|
||||||
value: '星期一'
|
},
|
||||||
},
|
check: {
|
||||||
{
|
type: Function,
|
||||||
key: 3,
|
default: () => {
|
||||||
value: '星期二'
|
}
|
||||||
},
|
}
|
||||||
{
|
})
|
||||||
key: 4,
|
const radioValue = ref(2)
|
||||||
value: '星期三'
|
const cycle01 = ref(2)
|
||||||
},
|
const cycle02 = ref(3)
|
||||||
{
|
const average01 = ref(1)
|
||||||
key: 5,
|
const average02 = ref(2)
|
||||||
value: '星期四'
|
const weekday = ref(2)
|
||||||
},
|
const checkboxList = ref([])
|
||||||
{
|
const checkCopy = ref([2])
|
||||||
key: 6,
|
const weekList = ref([
|
||||||
value: '星期五'
|
{key: 1, value: '星期日'},
|
||||||
},
|
{key: 2, value: '星期一'},
|
||||||
{
|
{key: 3, value: '星期二'},
|
||||||
key: 7,
|
{key: 4, value: '星期三'},
|
||||||
value: '星期六'
|
{key: 5, value: '星期四'},
|
||||||
},
|
{key: 6, value: '星期五'},
|
||||||
{
|
{key: 7, value: '星期六'}
|
||||||
key: 1,
|
])
|
||||||
value: '星期日'
|
const cycleTotal = computed(() => {
|
||||||
}
|
cycle01.value = props.check(cycle01.value, 1, 6)
|
||||||
],
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
|
||||||
checkNum: this.$options.propsData.check
|
return cycle01.value + '-' + cycle02.value
|
||||||
}
|
})
|
||||||
},
|
const averageTotal = computed(() => {
|
||||||
name: 'crontab-week',
|
average01.value = props.check(average01.value, 1, 4)
|
||||||
props: ['check', 'cron'],
|
average02.value = props.check(average02.value, 1, 7)
|
||||||
methods: {
|
return average02.value + '#' + average01.value
|
||||||
// 单选按钮值变化时
|
})
|
||||||
radioChange() {
|
const weekdayTotal = computed(() => {
|
||||||
if (this.radioValue !== 2 && this.cron.day !== '?') {
|
weekday.value = props.check(weekday.value, 1, 7)
|
||||||
this.$emit('update', 'day', '?', 'week')
|
return weekday.value + 'L'
|
||||||
}
|
})
|
||||||
switch (this.radioValue) {
|
const checkboxString = computed(() => {
|
||||||
case 1:
|
return checkboxList.value.join(',')
|
||||||
this.$emit('update', 'week', '*')
|
})
|
||||||
break
|
watch(() => props.cron.week, value => changeRadioValue(value))
|
||||||
case 2:
|
watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
|
||||||
this.$emit('update', 'week', '?')
|
function changeRadioValue(value) {
|
||||||
break
|
if (value === "*") {
|
||||||
case 3:
|
radioValue.value = 1
|
||||||
this.$emit('update', 'week', this.cycleTotal)
|
} else if (value === "?") {
|
||||||
break
|
radioValue.value = 2
|
||||||
case 4:
|
} else if (value.indexOf("-") > -1) {
|
||||||
this.$emit('update', 'week', this.averageTotal)
|
const indexArr = value.split('-')
|
||||||
break
|
cycle01.value = Number(indexArr[0])
|
||||||
case 5:
|
cycle02.value = Number(indexArr[1])
|
||||||
this.$emit('update', 'week', this.weekdayCheck + 'L')
|
radioValue.value = 3
|
||||||
break
|
} else if (value.indexOf("#") > -1) {
|
||||||
case 6:
|
const indexArr = value.split('#')
|
||||||
this.$emit('update', 'week', this.checkboxString)
|
average01.value = Number(indexArr[1])
|
||||||
break
|
average02.value = Number(indexArr[0])
|
||||||
}
|
radioValue.value = 4
|
||||||
},
|
} else if (value.indexOf("L") > -1) {
|
||||||
|
const indexArr = value.split("L")
|
||||||
// 周期两个值变化时
|
weekday.value = Number(indexArr[0])
|
||||||
cycleChange() {
|
radioValue.value = 5
|
||||||
if (this.radioValue == '3') {
|
} else {
|
||||||
this.$emit('update', 'week', this.cycleTotal)
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
}
|
radioValue.value = 6
|
||||||
},
|
}
|
||||||
// 平均两个值变化时
|
}
|
||||||
averageChange() {
|
function onRadioChange() {
|
||||||
if (this.radioValue == '4') {
|
if (radioValue.value === 2 && props.cron.day === '?') {
|
||||||
this.$emit('update', 'week', this.averageTotal)
|
emit('update', 'day', '*', 'week')
|
||||||
}
|
}
|
||||||
},
|
if (radioValue.value !== 2 && props.cron.day !== '?') {
|
||||||
// 最近工作日值变化时
|
emit('update', 'day', '?', 'week')
|
||||||
weekdayChange() {
|
}
|
||||||
if (this.radioValue == '5') {
|
switch (radioValue.value) {
|
||||||
this.$emit('update', 'week', this.weekday + 'L')
|
case 1:
|
||||||
}
|
emit('update', 'week', '*', 'week')
|
||||||
},
|
break
|
||||||
// checkbox值变化时
|
case 2:
|
||||||
checkboxChange() {
|
emit('update', 'week', '?', 'week')
|
||||||
if (this.radioValue == '6') {
|
break
|
||||||
this.$emit('update', 'week', this.checkboxString)
|
case 3:
|
||||||
}
|
emit('update', 'week', cycleTotal.value, 'week')
|
||||||
},
|
break
|
||||||
},
|
case 4:
|
||||||
watch: {
|
emit('update', 'week', averageTotal.value, 'week')
|
||||||
'radioValue': 'radioChange',
|
break
|
||||||
'cycleTotal': 'cycleChange',
|
case 5:
|
||||||
'averageTotal': 'averageChange',
|
emit('update', 'week', weekdayTotal.value, 'week')
|
||||||
'weekdayCheck': 'weekdayChange',
|
break
|
||||||
'checkboxString': 'checkboxChange',
|
case 6:
|
||||||
},
|
if (checkboxList.value.length === 0) {
|
||||||
computed: {
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
// 计算两个周期值
|
} else {
|
||||||
cycleTotal: function () {
|
checkCopy.value = checkboxList.value
|
||||||
this.cycle01 = this.checkNum(this.cycle01, 1, 7)
|
}
|
||||||
this.cycle02 = this.checkNum(this.cycle02, 1, 7)
|
emit('update', 'week', checkboxString.value, 'week')
|
||||||
return this.cycle01 + '-' + this.cycle02
|
break
|
||||||
},
|
}
|
||||||
// 计算平均用到的值
|
|
||||||
averageTotal: function () {
|
|
||||||
this.average01 = this.checkNum(this.average01, 1, 4)
|
|
||||||
this.average02 = this.checkNum(this.average02, 1, 7)
|
|
||||||
return this.average02 + '#' + this.average01
|
|
||||||
},
|
|
||||||
// 最近的工作日(格式)
|
|
||||||
weekdayCheck: function () {
|
|
||||||
this.weekday = this.checkNum(this.weekday, 1, 7)
|
|
||||||
return this.weekday
|
|
||||||
},
|
|
||||||
// 计算勾选的checkbox值合集
|
|
||||||
checkboxString: function () {
|
|
||||||
let str = this.checkboxList.join()
|
|
||||||
return str == '' ? '*' : str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
.el-select.multiselect, .el-select--small.multiselect {
|
||||||
|
width: 17.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,131 +1,143 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form size="small">
|
<el-form>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio :label="1" v-model='radioValue'>
|
<el-radio :value="1" v-model='radioValue'>
|
||||||
不填,允许的通配符[, - * /]
|
不填,允许的通配符[, - * /]
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio :label="2" v-model='radioValue'>
|
<el-radio :value="2" v-model='radioValue'>
|
||||||
每年
|
每年
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio :label="3" v-model='radioValue'>
|
<el-radio :value="3" v-model='radioValue'>
|
||||||
周期从
|
周期从
|
||||||
<el-input-number v-model='cycle01' :min='fullYear' :max="2098" /> -
|
<el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
|
||||||
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099" />
|
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio :label="4" v-model='radioValue'>
|
<el-radio :value="4" v-model='radioValue'>
|
||||||
从
|
从
|
||||||
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始,每
|
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始,每
|
||||||
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear" /> 年执行一次
|
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
|
||||||
</el-radio>
|
</el-radio>
|
||||||
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-radio :label="5" v-model='radioValue'>
|
<el-radio :value="5" v-model='radioValue'>
|
||||||
指定
|
指定
|
||||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple>
|
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
|
||||||
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
|
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update'])
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
cron: {
|
||||||
fullYear: 0,
|
type: Object,
|
||||||
radioValue: 1,
|
default: {
|
||||||
cycle01: 0,
|
second: "*",
|
||||||
cycle02: 0,
|
min: "*",
|
||||||
average01: 0,
|
hour: "*",
|
||||||
average02: 1,
|
day: "*",
|
||||||
checkboxList: [],
|
month: "*",
|
||||||
checkNum: this.$options.propsData.check
|
week: "?",
|
||||||
}
|
year: ""
|
||||||
},
|
}
|
||||||
name: 'crontab-year',
|
},
|
||||||
props: ['check', 'month', 'cron'],
|
check: {
|
||||||
methods: {
|
type: Function,
|
||||||
// 单选按钮值变化时
|
default: () => {
|
||||||
radioChange() {
|
}
|
||||||
switch (this.radioValue) {
|
}
|
||||||
case 1:
|
})
|
||||||
this.$emit('update', 'year', '')
|
|
||||||
break
|
const fullYear = Number(new Date().getFullYear())
|
||||||
case 2:
|
const maxFullYear = fullYear + 10
|
||||||
this.$emit('update', 'year', '*')
|
const radioValue = ref(1)
|
||||||
break
|
const cycle01 = ref(fullYear)
|
||||||
case 3:
|
const cycle02 = ref(fullYear + 1)
|
||||||
this.$emit('update', 'year', this.cycleTotal)
|
const average01 = ref(fullYear)
|
||||||
break
|
const average02 = ref(1)
|
||||||
case 4:
|
const checkboxList = ref([])
|
||||||
this.$emit('update', 'year', this.averageTotal)
|
const checkCopy = ref([fullYear])
|
||||||
break
|
|
||||||
case 5:
|
const cycleTotal = computed(() => {
|
||||||
this.$emit('update', 'year', this.checkboxString)
|
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
|
||||||
break
|
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
|
||||||
}
|
return cycle01.value + '-' + cycle02.value
|
||||||
},
|
})
|
||||||
// 周期两个值变化时
|
const averageTotal = computed(() => {
|
||||||
cycleChange() {
|
average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
|
||||||
if (this.radioValue == '3') {
|
average02.value = props.check(average02.value, 1, 10)
|
||||||
this.$emit('update', 'year', this.cycleTotal)
|
return average01.value + '/' + average02.value
|
||||||
}
|
})
|
||||||
},
|
const checkboxString = computed(() => {
|
||||||
// 平均两个值变化时
|
return checkboxList.value.join(',')
|
||||||
averageChange() {
|
})
|
||||||
if (this.radioValue == '4') {
|
watch(() => props.cron.year, value => changeRadioValue(value))
|
||||||
this.$emit('update', 'year', this.averageTotal)
|
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||||
}
|
function changeRadioValue(value) {
|
||||||
},
|
if (value === '') {
|
||||||
// checkbox值变化时
|
radioValue.value = 1
|
||||||
checkboxChange() {
|
} else if (value === "*") {
|
||||||
if (this.radioValue == '5') {
|
radioValue.value = 2
|
||||||
this.$emit('update', 'year', this.checkboxString)
|
} else if (value.indexOf("-") > -1) {
|
||||||
}
|
const indexArr = value.split('-')
|
||||||
}
|
cycle01.value = Number(indexArr[0])
|
||||||
},
|
cycle02.value = Number(indexArr[1])
|
||||||
watch: {
|
radioValue.value = 3
|
||||||
'radioValue': 'radioChange',
|
} else if (value.indexOf("/") > -1) {
|
||||||
'cycleTotal': 'cycleChange',
|
const indexArr = value.split('/')
|
||||||
'averageTotal': 'averageChange',
|
average01.value = Number(indexArr[0])
|
||||||
'checkboxString': 'checkboxChange'
|
average02.value = Number(indexArr[1])
|
||||||
},
|
radioValue.value = 4
|
||||||
computed: {
|
} else {
|
||||||
// 计算两个周期值
|
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||||
cycleTotal: function () {
|
radioValue.value = 5
|
||||||
const cycle01 = this.checkNum(this.cycle01, this.fullYear, 2098)
|
}
|
||||||
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : this.fullYear + 1, 2099)
|
}
|
||||||
return cycle01 + '-' + cycle02
|
function onRadioChange() {
|
||||||
},
|
switch (radioValue.value) {
|
||||||
// 计算平均用到的值
|
case 1:
|
||||||
averageTotal: function () {
|
emit('update', 'year', '', 'year')
|
||||||
const average01 = this.checkNum(this.average01, this.fullYear, 2098)
|
break
|
||||||
const average02 = this.checkNum(this.average02, 1, 2099 - average01 || this.fullYear)
|
case 2:
|
||||||
return average01 + '/' + average02
|
emit('update', 'year', '*', 'year')
|
||||||
},
|
break
|
||||||
// 计算勾选的checkbox值合集
|
case 3:
|
||||||
checkboxString: function () {
|
emit('update', 'year', cycleTotal.value, 'year')
|
||||||
let str = this.checkboxList.join()
|
break
|
||||||
return str
|
case 4:
|
||||||
}
|
emit('update', 'year', averageTotal.value, 'year')
|
||||||
},
|
break
|
||||||
mounted: function () {
|
case 5:
|
||||||
// 仅获取当前年份
|
if (checkboxList.value.length === 0) {
|
||||||
this.fullYear = Number(new Date().getFullYear())
|
checkboxList.value.push(checkCopy.value[0])
|
||||||
this.cycle01 = this.fullYear
|
} else {
|
||||||
this.average01 = this.fullYear
|
checkCopy.value = checkboxList.value
|
||||||
}
|
}
|
||||||
|
emit('update', 'year', checkboxString.value, 'year')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-input-number--small, .el-select, .el-select--small {
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.el-select, .el-select--small {
|
||||||
|
width: 18.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import store from '@/store'
|
|
||||||
import DataDict from '@/utils/dict'
|
|
||||||
import { getDicts as getDicts } from '@/api/system/dict/data'
|
|
||||||
|
|
||||||
function searchDictByKey(dict, key) {
|
|
||||||
if (key == null && key == "") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < dict.length; i++) {
|
|
||||||
if (dict[i].key == key) {
|
|
||||||
return dict[i].value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function install() {
|
|
||||||
Vue.use(DataDict, {
|
|
||||||
metas: {
|
|
||||||
'*': {
|
|
||||||
labelField: 'dictLabel',
|
|
||||||
valueField: 'dictValue',
|
|
||||||
request(dictMeta) {
|
|
||||||
const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
|
|
||||||
if (storeDict) {
|
|
||||||
return new Promise(resolve => { resolve(storeDict) })
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
getDicts(dictMeta.type).then(res => {
|
|
||||||
store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
|
|
||||||
resolve(res.data)
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
install,
|
|
||||||
}
|
|
||||||
@@ -3,22 +3,19 @@
|
|||||||
<template v-for="(item, index) in options">
|
<template v-for="(item, index) in options">
|
||||||
<template v-if="isValueMatch(item.value)">
|
<template v-if="isValueMatch(item.value)">
|
||||||
<span
|
<span
|
||||||
v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)"
|
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:index="index"
|
:index="index"
|
||||||
:class="item.raw.cssClass"
|
:class="item.elTagClass"
|
||||||
>{{ item.label + ' ' }}</span
|
>{{ item.label + " " }}</span>
|
||||||
>
|
|
||||||
<el-tag
|
<el-tag
|
||||||
v-else
|
v-else
|
||||||
:disable-transitions="true"
|
:disable-transitions="true"
|
||||||
:key="item.value"
|
:key="item.value + ''"
|
||||||
:index="index"
|
:index="index"
|
||||||
:type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
|
:type="item.elTagType"
|
||||||
:class="item.raw.cssClass"
|
:class="item.elTagClass"
|
||||||
>
|
>{{ item.label + " " }}</el-tag>
|
||||||
{{ item.label + ' ' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="unmatch && showValue">
|
<template v-if="unmatch && showValue">
|
||||||
@@ -27,65 +24,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
// 记录未匹配的项
|
||||||
name: "DictTag",
|
const unmatchArray = ref([])
|
||||||
props: {
|
|
||||||
options: {
|
const props = defineProps({
|
||||||
type: Array,
|
// 数据
|
||||||
default: null,
|
options: {
|
||||||
},
|
type: Array,
|
||||||
value: [Number, String, Array],
|
default: null,
|
||||||
showValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
type: String,
|
|
||||||
default: ","
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
// 当前的值
|
||||||
return {
|
value: [Number, String, Array],
|
||||||
unmatchArray: [], // 记录未匹配的项
|
// 当未找到匹配的数据时,显示value
|
||||||
}
|
showValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
computed: {
|
separator: {
|
||||||
values() {
|
type: String,
|
||||||
if (this.value === null || typeof this.value === 'undefined' || this.value === '') return []
|
default: ",",
|
||||||
if (typeof this.value === 'number' || typeof this.value === 'boolean') return [this.value]
|
|
||||||
return Array.isArray(this.value) ? this.value.map(item => '' + item) : String(this.value).split(this.separator)
|
|
||||||
},
|
|
||||||
unmatch() {
|
|
||||||
this.unmatchArray = []
|
|
||||||
// 没有value不显示
|
|
||||||
if (this.value === null || typeof this.value === 'undefined' || this.value === '' || this.options.length === 0) return false
|
|
||||||
// 传入值为数组
|
|
||||||
let unmatch = false // 添加一个标志来判断是否有未匹配项
|
|
||||||
this.values.forEach(item => {
|
|
||||||
if (!this.options.some(v => v.value == item)) {
|
|
||||||
this.unmatchArray.push(item)
|
|
||||||
unmatch = true // 如果有未匹配项,将标志设置为true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return unmatch // 返回标志的值
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isValueMatch(itemValue) {
|
|
||||||
return this.values.some(val => val == itemValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
handleArray(array) {
|
|
||||||
if (array.length === 0) return ''
|
|
||||||
return array.reduce((pre, cur) => {
|
|
||||||
return pre + ' ' + cur
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const values = computed(() => {
|
||||||
|
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
|
||||||
|
if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value]
|
||||||
|
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unmatch = computed(() => {
|
||||||
|
unmatchArray.value = []
|
||||||
|
// 没有value不显示
|
||||||
|
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
|
||||||
|
// 传入值为数组
|
||||||
|
let unmatch = false // 添加一个标志来判断是否有未匹配项
|
||||||
|
values.value.forEach(item => {
|
||||||
|
if (!props.options.some(v => v.value == item)) {
|
||||||
|
unmatchArray.value.push(item)
|
||||||
|
unmatch = true // 如果有未匹配项,将标志设置为true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return unmatch // 返回标志的值
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleArray(array) {
|
||||||
|
if (array.length === 0) return ""
|
||||||
|
return array.reduce((pre, cur) => {
|
||||||
|
return pre + " " + cur
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValueMatch(itemValue) {
|
||||||
|
return values.value.some(val => val == itemValue)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-tag + .el-tag {
|
.el-tag + .el-tag {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
|||||||
@@ -8,218 +8,197 @@
|
|||||||
name="file"
|
name="file"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
style="display: none"
|
class="editor-img-uploader"
|
||||||
ref="upload"
|
v-if="type == 'url'"
|
||||||
v-if="this.type == 'url'"
|
|
||||||
>
|
>
|
||||||
|
<i ref="uploadRef" class="editor-img-uploader"></i>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="editor" ref="editor" :style="styles"></div>
|
</div>
|
||||||
|
<div class="editor">
|
||||||
|
<quill-editor
|
||||||
|
ref="quillEditorRef"
|
||||||
|
v-model:content="content"
|
||||||
|
contentType="html"
|
||||||
|
@textChange="(e) => $emit('update:modelValue', content)"
|
||||||
|
:options="options"
|
||||||
|
:style="styles"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import Quill from "quill"
|
import { QuillEditor } from "@vueup/vue-quill"
|
||||||
import "quill/dist/quill.core.css"
|
import "@vueup/vue-quill/dist/vue-quill.snow.css"
|
||||||
import "quill/dist/quill.snow.css"
|
|
||||||
import "quill/dist/quill.bubble.css"
|
|
||||||
import { getToken } from "@/utils/auth"
|
import { getToken } from "@/utils/auth"
|
||||||
|
|
||||||
export default {
|
const { proxy } = getCurrentInstance()
|
||||||
name: "Editor",
|
|
||||||
props: {
|
const quillEditorRef = ref()
|
||||||
/* 编辑器的内容 */
|
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
|
||||||
value: {
|
const headers = ref({
|
||||||
type: String,
|
Authorization: "Bearer " + getToken()
|
||||||
default: "",
|
})
|
||||||
},
|
|
||||||
/* 高度 */
|
const props = defineProps({
|
||||||
height: {
|
/* 编辑器的内容 */
|
||||||
type: Number,
|
modelValue: {
|
||||||
default: null,
|
type: String,
|
||||||
},
|
|
||||||
/* 最小高度 */
|
|
||||||
minHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
/* 只读 */
|
|
||||||
readOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
/* 上传文件大小限制(MB) */
|
|
||||||
fileSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 5,
|
|
||||||
},
|
|
||||||
/* 类型(base64格式、url格式) */
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: "url",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
/* 高度 */
|
||||||
return {
|
height: {
|
||||||
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
|
type: Number,
|
||||||
headers: {
|
default: null,
|
||||||
Authorization: "Bearer " + getToken()
|
|
||||||
},
|
|
||||||
Quill: null,
|
|
||||||
currentValue: "",
|
|
||||||
options: {
|
|
||||||
theme: "snow",
|
|
||||||
bounds: document.body,
|
|
||||||
debug: "warn",
|
|
||||||
modules: {
|
|
||||||
// 工具栏配置
|
|
||||||
toolbar: [
|
|
||||||
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
|
|
||||||
["blockquote", "code-block"], // 引用 代码块
|
|
||||||
[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
|
|
||||||
[{ indent: "-1" }, { indent: "+1" }], // 缩进
|
|
||||||
[{ size: ["small", false, "large", "huge"] }], // 字体大小
|
|
||||||
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
|
||||||
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
|
||||||
[{ align: [] }], // 对齐方式
|
|
||||||
["clean"], // 清除文本格式
|
|
||||||
["link", "image", "video"] // 链接、图片、视频
|
|
||||||
],
|
|
||||||
},
|
|
||||||
placeholder: "请输入内容",
|
|
||||||
readOnly: this.readOnly,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
/* 最小高度 */
|
||||||
styles() {
|
minHeight: {
|
||||||
let style = {}
|
type: Number,
|
||||||
if (this.minHeight) {
|
default: null,
|
||||||
style.minHeight = `${this.minHeight}px`
|
|
||||||
}
|
|
||||||
if (this.height) {
|
|
||||||
style.height = `${this.height}px`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
/* 只读 */
|
||||||
value: {
|
readOnly: {
|
||||||
handler(val) {
|
type: Boolean,
|
||||||
if (val !== this.currentValue) {
|
default: false,
|
||||||
this.currentValue = val === null ? "" : val
|
|
||||||
if (this.Quill) {
|
|
||||||
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
/* 上传文件大小限制(MB) */
|
||||||
this.init()
|
fileSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
/* 类型(base64格式、url格式) */
|
||||||
this.Quill = null
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "url",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = ref({
|
||||||
|
theme: "snow",
|
||||||
|
bounds: document.body,
|
||||||
|
debug: "warn",
|
||||||
|
modules: {
|
||||||
|
// 工具栏配置
|
||||||
|
toolbar: [
|
||||||
|
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
|
||||||
|
["blockquote", "code-block"], // 引用 代码块
|
||||||
|
[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
|
||||||
|
[{ indent: "-1" }, { indent: "+1" }], // 缩进
|
||||||
|
[{ size: ["small", false, "large", "huge"] }], // 字体大小
|
||||||
|
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
||||||
|
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
||||||
|
[{ align: [] }], // 对齐方式
|
||||||
|
["clean"], // 清除文本格式
|
||||||
|
["link", "image", "video"] // 链接、图片、视频
|
||||||
|
],
|
||||||
},
|
},
|
||||||
methods: {
|
placeholder: "请输入内容",
|
||||||
init() {
|
readOnly: props.readOnly
|
||||||
const editor = this.$refs.editor
|
})
|
||||||
this.Quill = new Quill(editor, this.options)
|
|
||||||
// 如果设置了上传地址则自定义图片上传事件
|
const styles = computed(() => {
|
||||||
if (this.type == 'url') {
|
let style = {}
|
||||||
let toolbar = this.Quill.getModule("toolbar")
|
if (props.minHeight) {
|
||||||
toolbar.addHandler("image", (value) => {
|
style.minHeight = `${props.minHeight}px`
|
||||||
if (value) {
|
}
|
||||||
this.$refs.upload.$children[0].$refs.input.click()
|
if (props.height) {
|
||||||
} else {
|
style.height = `${props.height}px`
|
||||||
this.quill.format("image", false)
|
}
|
||||||
}
|
return style
|
||||||
})
|
})
|
||||||
this.Quill.root.addEventListener('paste', this.handlePasteCapture, true)
|
|
||||||
}
|
const content = ref("")
|
||||||
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue)
|
watch(() => props.modelValue, (v) => {
|
||||||
this.Quill.on("text-change", (delta, oldDelta, source) => {
|
if (v !== content.value) {
|
||||||
const html = this.$refs.editor.children[0].innerHTML
|
content.value = v == undefined ? "<p></p>" : v
|
||||||
const text = this.Quill.getText()
|
}
|
||||||
const quill = this.Quill
|
}, { immediate: true })
|
||||||
this.currentValue = html
|
|
||||||
this.$emit("input", html)
|
// 如果设置了上传地址则自定义图片上传事件
|
||||||
this.$emit("on-change", { html, text, quill })
|
onMounted(() => {
|
||||||
})
|
if (props.type == 'url') {
|
||||||
this.Quill.on("text-change", (delta, oldDelta, source) => {
|
let quill = quillEditorRef.value.getQuill()
|
||||||
this.$emit("on-text-change", delta, oldDelta, source)
|
let toolbar = quill.getModule("toolbar")
|
||||||
})
|
toolbar.addHandler("image", (value) => {
|
||||||
this.Quill.on("selection-change", (range, oldRange, source) => {
|
if (value) {
|
||||||
this.$emit("on-selection-change", range, oldRange, source)
|
proxy.$refs.uploadRef.click()
|
||||||
})
|
|
||||||
this.Quill.on("editor-change", (eventName, ...args) => {
|
|
||||||
this.$emit("on-editor-change", eventName, ...args)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 上传前校检格式和大小
|
|
||||||
handleBeforeUpload(file) {
|
|
||||||
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
|
|
||||||
const isJPG = type.includes(file.type)
|
|
||||||
// 检验文件格式
|
|
||||||
if (!isJPG) {
|
|
||||||
this.$message.error(`图片格式错误!`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 校检文件大小
|
|
||||||
if (this.fileSize) {
|
|
||||||
const isLt = file.size / 1024 / 1024 < this.fileSize
|
|
||||||
if (!isLt) {
|
|
||||||
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
handleUploadSuccess(res, file) {
|
|
||||||
// 如果上传成功
|
|
||||||
if (res.code == 200) {
|
|
||||||
// 获取富文本组件实例
|
|
||||||
let quill = this.Quill
|
|
||||||
// 获取光标所在位置
|
|
||||||
let length = quill.getSelection().index
|
|
||||||
// 插入图片 res.url为服务器返回的图片地址
|
|
||||||
quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName)
|
|
||||||
// 调整光标到最后
|
|
||||||
quill.setSelection(length + 1)
|
|
||||||
} else {
|
} else {
|
||||||
this.$message.error("图片插入失败")
|
quill.format("image", false)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
handleUploadError() {
|
quill.root.addEventListener('paste', handlePasteCapture, true)
|
||||||
this.$message.error("图片插入失败")
|
}
|
||||||
},
|
})
|
||||||
// 复制粘贴图片处理
|
|
||||||
handlePasteCapture(e) {
|
// 上传前校检格式和大小
|
||||||
const clipboard = e.clipboardData || window.clipboardData
|
function handleBeforeUpload(file) {
|
||||||
if (clipboard && clipboard.items) {
|
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
|
||||||
for (let i = 0; i < clipboard.items.length; i++) {
|
const isJPG = type.includes(file.type)
|
||||||
const item = clipboard.items[i]
|
//检验文件格式
|
||||||
if (item.type.indexOf('image') !== -1) {
|
if (!isJPG) {
|
||||||
e.preventDefault()
|
proxy.$modal.msgError(`图片格式错误!`)
|
||||||
const file = item.getAsFile()
|
return false
|
||||||
this.insertImage(file)
|
}
|
||||||
}
|
// 校检文件大小
|
||||||
}
|
if (props.fileSize) {
|
||||||
}
|
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||||
},
|
if (!isLt) {
|
||||||
insertImage(file) {
|
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
|
||||||
const formData = new FormData()
|
return false
|
||||||
formData.append("file", file)
|
|
||||||
axios.post(this.uploadUrl, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: this.headers.Authorization } }).then(res => {
|
|
||||||
this.handleUploadSuccess(res.data)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功处理
|
||||||
|
function handleUploadSuccess(res, file) {
|
||||||
|
// 如果上传成功
|
||||||
|
if (res.code == 200) {
|
||||||
|
// 获取富文本实例
|
||||||
|
let quill = toRaw(quillEditorRef.value).getQuill()
|
||||||
|
// 获取光标位置
|
||||||
|
let length = quill.selection.savedRange.index
|
||||||
|
// 插入图片,res.url为服务器返回的图片链接地址
|
||||||
|
quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName)
|
||||||
|
// 调整光标到最后
|
||||||
|
quill.setSelection(length + 1)
|
||||||
|
} else {
|
||||||
|
proxy.$modal.msgError("图片插入失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传失败处理
|
||||||
|
function handleUploadError() {
|
||||||
|
proxy.$modal.msgError("图片插入失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制粘贴图片处理
|
||||||
|
function handlePasteCapture(e) {
|
||||||
|
const clipboard = e.clipboardData || window.clipboardData
|
||||||
|
if (clipboard && clipboard.items) {
|
||||||
|
for (let i = 0; i < clipboard.items.length; i++) {
|
||||||
|
const item = clipboard.items[i]
|
||||||
|
if (item.type.indexOf('image') !== -1) {
|
||||||
|
e.preventDefault()
|
||||||
|
const file = item.getAsFile()
|
||||||
|
insertImage(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImage(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
|
||||||
|
handleUploadSuccess(res.data)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.editor-img-uploader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.editor, .ql-toolbar {
|
.editor, .ql-toolbar {
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
line-height: normal !important;
|
line-height: normal !important;
|
||||||
|
|||||||
@@ -1,126 +1,137 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="visible" :width="width" append-to-body @close="handleClose">
|
<el-dialog :title="title" v-model="visible" :width="width" append-to-body @close="handleClose">
|
||||||
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-success="handleSuccess" :auto-upload="false" drag>
|
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-change="handleFileChange" :on-remove="handleFileRemove" :on-success="handleSuccess" :auto-upload="false" drag>
|
||||||
<i class="el-icon-upload"></i>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||||
<div class="el-upload__tip text-center" slot="tip">
|
<template #tip>
|
||||||
<div class="el-upload__tip" slot="tip">
|
<div class="el-upload__tip text-center">
|
||||||
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
<div class="el-upload__tip">
|
||||||
|
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
||||||
|
</div>
|
||||||
|
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||||
|
<el-link v-if="templateUrl" type="primary" underline="never" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
|
||||||
</div>
|
</div>
|
||||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
</template>
|
||||||
<el-link v-if="templateUrl" type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
|
|
||||||
</div>
|
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button @click="visible = false">取 消</el-button>
|
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||||
</div>
|
<el-button @click="visible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { getToken } from '@/utils/auth'
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
export default {
|
const { proxy } = getCurrentInstance()
|
||||||
props: {
|
|
||||||
// 对话框标题
|
const props = defineProps({
|
||||||
title: {
|
// 对话框标题
|
||||||
type: String,
|
title: {
|
||||||
default: '数据导入'
|
type: String,
|
||||||
},
|
default: '数据导入'
|
||||||
// 对话框宽度
|
|
||||||
width: {
|
|
||||||
type: String,
|
|
||||||
default: '400px'
|
|
||||||
},
|
|
||||||
// 上传接口地址(必传)
|
|
||||||
action: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
// 模板下载接口地址,不传则不显示下载模板链接
|
|
||||||
templateAction: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
// 模板文件名
|
|
||||||
templateFileName: {
|
|
||||||
type: String,
|
|
||||||
default: 'template'
|
|
||||||
},
|
|
||||||
// 覆盖更新勾选框的说明文字
|
|
||||||
updateSupportLabel: {
|
|
||||||
type: String,
|
|
||||||
default: '是否更新已经存在的数据'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
// 对话框宽度
|
||||||
return {
|
width: {
|
||||||
visible: false,
|
type: String,
|
||||||
isUploading: false,
|
default: '400px'
|
||||||
updateSupport: false,
|
|
||||||
headers: { Authorization: 'Bearer ' + getToken() }
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
// 上传接口地址(必传)
|
||||||
uploadUrl() {
|
action: {
|
||||||
return process.env.VUE_APP_BASE_API + this.action + '?updateSupport=' + (this.updateSupport ? 1 : 0)
|
type: String,
|
||||||
},
|
required: true
|
||||||
templateUrl() {
|
|
||||||
return !!this.templateAction
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
// 模板下载接口地址,不传则不显示下载模板链接
|
||||||
// 打开对话框(供父组件通过 ref 调用)
|
templateAction: {
|
||||||
open() {
|
type: String,
|
||||||
this.updateSupport = false
|
default: ''
|
||||||
this.isUploading = false
|
},
|
||||||
this.visible = true
|
// 模板文件名前缀
|
||||||
this.$nextTick(() => {
|
templateFileName: {
|
||||||
if (this.$refs.uploadRef) {
|
type: String,
|
||||||
this.$refs.uploadRef.clearFiles()
|
default: 'template'
|
||||||
}
|
},
|
||||||
})
|
// 覆盖更新勾选框的说明文字
|
||||||
},
|
updateSupportLabel: {
|
||||||
// 关闭时清理
|
type: String,
|
||||||
handleClose() {
|
default: '是否更新已经存在的数据'
|
||||||
this.isUploading = false
|
|
||||||
if (this.$refs.uploadRef) {
|
|
||||||
this.$refs.uploadRef.clearFiles()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 下载模板
|
|
||||||
handleDownloadTemplate() {
|
|
||||||
this.download(this.templateAction, {}, `${this.templateFileName}_${new Date().getTime()}.xlsx`)
|
|
||||||
},
|
|
||||||
// 上传进度
|
|
||||||
handleProgress() {
|
|
||||||
this.isUploading = true
|
|
||||||
},
|
|
||||||
// 上传成功
|
|
||||||
handleSuccess(response) {
|
|
||||||
this.visible = false
|
|
||||||
this.isUploading = false
|
|
||||||
if (this.$refs.uploadRef) {
|
|
||||||
this.$refs.uploadRef.clearFiles()
|
|
||||||
}
|
|
||||||
this.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
|
||||||
this.$emit('success')
|
|
||||||
},
|
|
||||||
// 提交上传
|
|
||||||
handleSubmit() {
|
|
||||||
const files = this.$refs.uploadRef.uploadFiles
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
this.$modal.msgError('请选择要上传的文件。')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const name = files[0].name.toLowerCase()
|
|
||||||
if (!name.endsWith('.xls') && !name.endsWith('.xlsx')) {
|
|
||||||
this.$modal.msgError('请选择后缀为 "xls" 或 "xlsx" 的文件。')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$refs.uploadRef.submit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
const uploadRef = ref(null)
|
||||||
|
const visible = ref(false)
|
||||||
|
const selectedFile = ref(null)
|
||||||
|
const isUploading = ref(false)
|
||||||
|
const updateSupport = ref(false)
|
||||||
|
const headers = { Authorization: 'Bearer ' + getToken() }
|
||||||
|
|
||||||
|
const uploadUrl = computed(() => {
|
||||||
|
return import.meta.env.VITE_APP_BASE_API + props.action + '?updateSupport=' + (updateSupport.value ? 1 : 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const templateUrl = computed(() => !!props.templateAction)
|
||||||
|
|
||||||
|
// 打开对话框(供父组件通过 ref 调用)
|
||||||
|
function open() {
|
||||||
|
updateSupport.value = false
|
||||||
|
isUploading.value = false
|
||||||
|
visible.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
selectedFile.value = null
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭时清理
|
||||||
|
function handleClose() {
|
||||||
|
isUploading.value = false
|
||||||
|
selectedFile.value = null
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载模板
|
||||||
|
function handleDownloadTemplate() {
|
||||||
|
proxy.download(props.templateAction, {}, `${props.templateFileName}_${new Date().getTime()}.xlsx`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传进度
|
||||||
|
function handleProgress() {
|
||||||
|
isUploading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文件选择处理 */
|
||||||
|
const handleFileChange = (file, fileList) => {
|
||||||
|
selectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文件删除处理 */
|
||||||
|
const handleFileRemove = (file, fileList) => {
|
||||||
|
selectedFile.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功
|
||||||
|
function handleSuccess(response) {
|
||||||
|
visible.value = false
|
||||||
|
isUploading.value = false
|
||||||
|
selectedFile.value = null
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
proxy.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交上传
|
||||||
|
function handleSubmit() {
|
||||||
|
const file = selectedFile.value
|
||||||
|
if (!file || file.length === 0 || !file.name.toLowerCase().endsWith('.xls') && !file.name.toLowerCase().endsWith('.xlsx')) {
|
||||||
|
proxy.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadRef.value.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,225 +17,218 @@
|
|||||||
v-if="!disabled"
|
v-if="!disabled"
|
||||||
>
|
>
|
||||||
<!-- 上传按钮 -->
|
<!-- 上传按钮 -->
|
||||||
<el-button size="mini" type="primary">选取文件</el-button>
|
<el-button type="primary">选取文件</el-button>
|
||||||
<!-- 上传提示 -->
|
|
||||||
<div class="el-upload__tip" slot="tip" v-if="showTip">
|
|
||||||
请上传
|
|
||||||
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
|
|
||||||
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
|
|
||||||
的文件
|
|
||||||
</div>
|
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<!-- 上传提示 -->
|
||||||
|
<div class="el-upload__tip" v-if="showTip && !disabled">
|
||||||
|
请上传
|
||||||
|
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
|
||||||
|
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
|
||||||
|
的文件
|
||||||
|
</div>
|
||||||
<!-- 文件列表 -->
|
<!-- 文件列表 -->
|
||||||
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||||||
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
|
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
|
||||||
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
|
<el-link :href="`${baseUrl}${file.url}`" underline="never" target="_blank">
|
||||||
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
||||||
</el-link>
|
</el-link>
|
||||||
<div class="ele-upload-list__item-content-action">
|
<div class="ele-upload-list__item-content-action">
|
||||||
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">删除</el-link>
|
<el-link underline="never" @click="handleDelete(index)" type="danger" v-if="!disabled"> 删除</el-link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { getToken } from "@/utils/auth"
|
import { getToken } from "@/utils/auth"
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: "FileUpload",
|
modelValue: [String, Object, Array],
|
||||||
props: {
|
// 上传接口地址
|
||||||
// 值
|
action: {
|
||||||
value: [String, Object, Array],
|
type: String,
|
||||||
// 上传接口地址
|
default: "/common/upload"
|
||||||
action: {
|
|
||||||
type: String,
|
|
||||||
default: "/common/upload"
|
|
||||||
},
|
|
||||||
// 上传携带的参数
|
|
||||||
data: {
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
// 数量限制
|
|
||||||
limit: {
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
// 大小限制(MB)
|
|
||||||
fileSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
|
||||||
fileType: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
|
|
||||||
},
|
|
||||||
// 是否显示提示
|
|
||||||
isShowTip: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
// 禁用组件(仅查看文件)
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 拖动排序
|
|
||||||
drag: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
// 上传携带的参数
|
||||||
return {
|
data: {
|
||||||
number: 0,
|
type: Object
|
||||||
uploadList: [],
|
|
||||||
baseUrl: process.env.VUE_APP_BASE_API,
|
|
||||||
uploadFileUrl: process.env.VUE_APP_BASE_API + this.action, // 上传文件服务器地址
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + getToken(),
|
|
||||||
},
|
|
||||||
fileList: []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
// 数量限制
|
||||||
if (this.drag && !this.disabled) {
|
limit: {
|
||||||
this.$nextTick(() => {
|
type: Number,
|
||||||
const element = this.$refs.uploadFileList?.$el || this.$refs.uploadFileList
|
default: 5
|
||||||
Sortable.create(element, {
|
|
||||||
ghostClass: 'file-upload-darg',
|
|
||||||
onEnd: (evt) => {
|
|
||||||
const movedItem = this.fileList.splice(evt.oldIndex, 1)[0]
|
|
||||||
this.fileList.splice(evt.newIndex, 0, movedItem)
|
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
// 大小限制(MB)
|
||||||
value: {
|
fileSize: {
|
||||||
handler(val) {
|
type: Number,
|
||||||
if (val) {
|
default: 5
|
||||||
let temp = 1
|
|
||||||
// 首先将值转为数组
|
|
||||||
const list = Array.isArray(val) ? val : this.value.split(',')
|
|
||||||
// 然后将数组转为对象数组
|
|
||||||
this.fileList = list.map(item => {
|
|
||||||
if (typeof item === "string") {
|
|
||||||
item = { name: item, url: item }
|
|
||||||
}
|
|
||||||
item.uid = item.uid || new Date().getTime() + temp++
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.fileList = []
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||||
// 是否显示提示
|
fileType: {
|
||||||
showTip() {
|
type: Array,
|
||||||
return this.isShowTip && (this.fileType || this.fileSize)
|
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
// 是否显示提示
|
||||||
// 上传前校检格式和大小
|
isShowTip: {
|
||||||
handleBeforeUpload(file) {
|
type: Boolean,
|
||||||
// 校检文件类型
|
default: true
|
||||||
if (this.fileType) {
|
},
|
||||||
const fileName = file.name.split('.')
|
// 禁用组件(仅查看文件)
|
||||||
const fileExt = fileName[fileName.length - 1]
|
disabled: {
|
||||||
const isTypeOk = this.fileType.indexOf(fileExt) >= 0
|
type: Boolean,
|
||||||
if (!isTypeOk) {
|
default: false
|
||||||
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}格式文件!`)
|
},
|
||||||
return false
|
// 拖动排序
|
||||||
}
|
drag: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const emit = defineEmits()
|
||||||
|
const number = ref(0)
|
||||||
|
const uploadList = ref([])
|
||||||
|
const baseUrl = import.meta.env.VITE_APP_BASE_API
|
||||||
|
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
|
||||||
|
const headers = ref({ Authorization: "Bearer " + getToken() })
|
||||||
|
const fileList = ref([])
|
||||||
|
const showTip = computed(
|
||||||
|
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, val => {
|
||||||
|
if (val) {
|
||||||
|
let temp = 1
|
||||||
|
// 首先将值转为数组
|
||||||
|
const list = Array.isArray(val) ? val : props.modelValue.split(',')
|
||||||
|
// 然后将数组转为对象数组
|
||||||
|
fileList.value = list.map(item => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
item = { name: item, url: item }
|
||||||
}
|
}
|
||||||
// 校检文件名是否包含特殊字符
|
item.uid = item.uid || new Date().getTime() + temp++
|
||||||
if (file.name.includes(',')) {
|
return item
|
||||||
this.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
})
|
||||||
return false
|
} else {
|
||||||
}
|
fileList.value = []
|
||||||
// 校检文件大小
|
return []
|
||||||
if (this.fileSize) {
|
}
|
||||||
const isLt = file.size / 1024 / 1024 < this.fileSize
|
},{ deep: true, immediate: true })
|
||||||
if (!isLt) {
|
|
||||||
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`)
|
// 上传前校检格式和大小
|
||||||
return false
|
function handleBeforeUpload(file) {
|
||||||
}
|
// 校检文件类型
|
||||||
}
|
if (props.fileType.length) {
|
||||||
this.$modal.loading("正在上传文件,请稍候...")
|
const fileName = file.name.split('.')
|
||||||
this.number++
|
const fileExt = fileName[fileName.length - 1]
|
||||||
return true
|
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
|
||||||
},
|
if (!isTypeOk) {
|
||||||
// 文件个数超出
|
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
|
||||||
handleExceed() {
|
return false
|
||||||
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`)
|
|
||||||
},
|
|
||||||
// 上传失败
|
|
||||||
handleUploadError(err) {
|
|
||||||
this.$modal.msgError("上传文件失败,请重试")
|
|
||||||
this.$modal.closeLoading()
|
|
||||||
},
|
|
||||||
// 上传成功回调
|
|
||||||
handleUploadSuccess(res, file) {
|
|
||||||
if (res.code === 200) {
|
|
||||||
this.uploadList.push({ name: res.fileName, url: res.fileName })
|
|
||||||
this.uploadedSuccessfully()
|
|
||||||
} else {
|
|
||||||
this.number--
|
|
||||||
this.$modal.closeLoading()
|
|
||||||
this.$modal.msgError(res.msg)
|
|
||||||
this.$refs.fileUpload.handleRemove(file)
|
|
||||||
this.uploadedSuccessfully()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 删除文件
|
|
||||||
handleDelete(index) {
|
|
||||||
this.fileList.splice(index, 1)
|
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
|
||||||
},
|
|
||||||
// 上传结束处理
|
|
||||||
uploadedSuccessfully() {
|
|
||||||
if (this.number > 0 && this.uploadList.length === this.number) {
|
|
||||||
this.fileList = this.fileList.concat(this.uploadList)
|
|
||||||
this.uploadList = []
|
|
||||||
this.number = 0
|
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
|
||||||
this.$modal.closeLoading()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 获取文件名称
|
|
||||||
getFileName(name) {
|
|
||||||
// 如果是url那么取最后的名字 如果不是直接返回
|
|
||||||
if (name.lastIndexOf("/") > -1) {
|
|
||||||
return name.slice(name.lastIndexOf("/") + 1)
|
|
||||||
} else {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 对象转成指定字符串分隔
|
|
||||||
listToString(list, separator) {
|
|
||||||
let strs = ""
|
|
||||||
separator = separator || ","
|
|
||||||
for (let i in list) {
|
|
||||||
strs += list[i].url + separator
|
|
||||||
}
|
|
||||||
return strs != '' ? strs.substr(0, strs.length - 1) : ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 校检文件名是否包含特殊字符
|
||||||
|
if (file.name.includes(',')) {
|
||||||
|
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 校检文件大小
|
||||||
|
if (props.fileSize) {
|
||||||
|
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||||
|
if (!isLt) {
|
||||||
|
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxy.$modal.loading("正在上传文件,请稍候...")
|
||||||
|
number.value++
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
|
// 文件个数超出
|
||||||
|
function handleExceed() {
|
||||||
|
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传失败
|
||||||
|
function handleUploadError(err) {
|
||||||
|
proxy.$modal.msgError("上传文件失败")
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功回调
|
||||||
|
function handleUploadSuccess(res, file) {
|
||||||
|
if (res.code === 200) {
|
||||||
|
uploadList.value.push({ name: res.fileName, url: res.fileName })
|
||||||
|
uploadedSuccessfully()
|
||||||
|
} else {
|
||||||
|
number.value--
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
proxy.$modal.msgError(res.msg)
|
||||||
|
proxy.$refs.fileUpload.handleRemove(file)
|
||||||
|
uploadedSuccessfully()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
function handleDelete(index) {
|
||||||
|
fileList.value.splice(index, 1)
|
||||||
|
emit("update:modelValue", listToString(fileList.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传结束处理
|
||||||
|
function uploadedSuccessfully() {
|
||||||
|
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||||
|
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
|
||||||
|
uploadList.value = []
|
||||||
|
number.value = 0
|
||||||
|
emit("update:modelValue", listToString(fileList.value))
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件名称
|
||||||
|
function getFileName(name) {
|
||||||
|
// 如果是url那么取最后的名字 如果不是直接返回
|
||||||
|
if (name.lastIndexOf("/") > -1) {
|
||||||
|
return name.slice(name.lastIndexOf("/") + 1)
|
||||||
|
} else {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对象转成指定字符串分隔
|
||||||
|
function listToString(list, separator) {
|
||||||
|
let strs = ""
|
||||||
|
separator = separator || ","
|
||||||
|
for (let i in list) {
|
||||||
|
if (list[i].url) {
|
||||||
|
strs += list[i].url + separator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strs != '' ? strs.substr(0, strs.length - 1) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化拖拽排序
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.drag && !props.disabled) {
|
||||||
|
nextTick(() => {
|
||||||
|
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
|
||||||
|
Sortable.create(element, {
|
||||||
|
ghostClass: 'file-upload-darg',
|
||||||
|
onEnd: (evt) => {
|
||||||
|
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
|
||||||
|
fileList.value.splice(evt.newIndex, 0, movedItem)
|
||||||
|
emit('update:modelValue', listToString(fileList.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.file-upload-darg {
|
.file-upload-darg {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -249,6 +242,7 @@ export default {
|
|||||||
line-height: 2;
|
line-height: 2;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: none !important;
|
||||||
}
|
}
|
||||||
.upload-file-list .ele-upload-list__item-content {
|
.upload-file-list .ele-upload-list__item-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -7,26 +7,24 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="64"
|
width="64"
|
||||||
height="64"
|
height="64"
|
||||||
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
name: 'Hamburger',
|
isActive: {
|
||||||
props: {
|
type: Boolean,
|
||||||
isActive: {
|
default: false
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleClick() {
|
|
||||||
this.$emit('toggleClick')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits()
|
||||||
|
const toggleClick = () => {
|
||||||
|
emit('toggleClick')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="header-search">
|
<div class="header-search">
|
||||||
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
|
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible.sync="show"
|
v-model="show"
|
||||||
width="600px"
|
width="600"
|
||||||
@close="close"
|
@close="close"
|
||||||
@opened="onDialogOpened"
|
@opened="onDialogOpened"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
ref="headerSearchSelectRef"
|
ref="headerSearchSelectRef"
|
||||||
size="large"
|
size="large"
|
||||||
@input="querySearch"
|
@input="querySearch"
|
||||||
prefix-icon="el-icon-search"
|
prefix-icon="Search"
|
||||||
placeholder="菜单搜索,支持标题、URL模糊查询"
|
placeholder="菜单搜索,支持标题、URL模糊查询"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter.native="selectActiveResult"
|
@keyup.enter="selectActiveResult"
|
||||||
@keydown.up.native="navigateResult('up')"
|
@keydown.up.prevent="navigateResult('up')"
|
||||||
@keydown.down.native="navigateResult('down')"
|
@keydown.down.prevent="navigateResult('down')"
|
||||||
>
|
>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
@@ -27,11 +27,13 @@
|
|||||||
找到 <strong>{{ options.length }}</strong> 个结果
|
找到 <strong>{{ options.length }}</strong> 个结果
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-scrollbar wrap-class="right-scrollbar-wrapper">
|
<div class="result-wrap">
|
||||||
<div class="result-wrap">
|
<el-scrollbar>
|
||||||
|
|
||||||
<template v-if="options.length > 0">
|
<template v-if="options.length > 0">
|
||||||
<div
|
<div
|
||||||
class="search-item"
|
class="search-item"
|
||||||
|
tabindex="1"
|
||||||
v-for="(item, index) in options"
|
v-for="(item, index) in options"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:class="{ 'is-active': index === activeIndex }"
|
:class="{ 'is-active': index === activeIndex }"
|
||||||
@@ -51,13 +53,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="empty-state" v-else-if="search && options.length === 0">
|
<div class="empty-state" v-else-if="search && options.length === 0">
|
||||||
<i class="el-icon-search empty-icon"></i>
|
<el-icon class="empty-icon"><Search /></el-icon>
|
||||||
<p class="empty-text">未找到 "<strong>{{ search }}</strong>" 相关菜单</p>
|
<p class="empty-text">未找到 "<strong>{{ search }}</strong>" 相关菜单</p>
|
||||||
<p class="empty-tip">试试其他关键词或路径</p>
|
<p class="empty-tip">试试其他关键词或路径</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</el-scrollbar>
|
||||||
</el-scrollbar>
|
</div>
|
||||||
|
|
||||||
<div class="search-footer">
|
<div class="search-footer">
|
||||||
<span class="shortcut-item">
|
<span class="shortcut-item">
|
||||||
@@ -74,190 +76,188 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import Fuse from 'fuse.js/dist/fuse.min.js'
|
import Fuse from 'fuse.js'
|
||||||
import path from 'path'
|
import { getNormalPath } from '@/utils/ruoyi'
|
||||||
import { isHttp } from '@/utils/validate'
|
import { isHttp } from '@/utils/validate'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
export default {
|
const search = ref('')
|
||||||
name: 'HeaderSearch',
|
const options = ref([])
|
||||||
data() {
|
const searchPool = ref([])
|
||||||
return {
|
const activeIndex = ref(-1)
|
||||||
search: '',
|
const show = ref(false)
|
||||||
options: [],
|
const fuse = ref(undefined)
|
||||||
searchPool: [],
|
const headerSearchSelectRef = ref(null)
|
||||||
activeIndex: -1,
|
const router = useRouter()
|
||||||
show: false,
|
const theme = computed(() => useSettingsStore().theme)
|
||||||
fuse: undefined
|
const routes = computed(() => usePermissionStore().defaultRoutes)
|
||||||
}
|
|
||||||
},
|
function click() {
|
||||||
computed: {
|
show.value = !show.value
|
||||||
theme() {
|
if (show.value) {
|
||||||
return this.$store.state.settings.theme
|
options.value = searchPool.value
|
||||||
},
|
|
||||||
routes() {
|
|
||||||
return this.$store.getters.defaultRoutes
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
routes() {
|
|
||||||
this.searchPool = this.generateRoutes(this.routes)
|
|
||||||
},
|
|
||||||
searchPool(list) {
|
|
||||||
this.initFuse(list)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.searchPool = this.generateRoutes(this.routes)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
click() {
|
|
||||||
this.show = !this.show
|
|
||||||
if (this.show) {
|
|
||||||
this.options = this.searchPool
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDialogOpened() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.blur()
|
|
||||||
this.search = ''
|
|
||||||
this.options = this.searchPool
|
|
||||||
this.show = false
|
|
||||||
this.activeIndex = -1
|
|
||||||
},
|
|
||||||
change(val) {
|
|
||||||
const p = val.path
|
|
||||||
const query = val.query
|
|
||||||
if (isHttp(val.path)) {
|
|
||||||
// http(s):// 路径新窗口打开
|
|
||||||
const pindex = p.indexOf('http')
|
|
||||||
window.open(p.substr(pindex, p.length), '_blank')
|
|
||||||
} else {
|
|
||||||
if (query) {
|
|
||||||
this.$router.push({ path: p, query: JSON.parse(query) })
|
|
||||||
} else {
|
|
||||||
this.$router.push(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.search = ''
|
|
||||||
this.options = this.searchPool
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.show = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initFuse(list) {
|
|
||||||
this.fuse = new Fuse(list, {
|
|
||||||
shouldSort: true,
|
|
||||||
threshold: 0.2,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
keys: [{
|
|
||||||
name: 'title',
|
|
||||||
weight: 0.7
|
|
||||||
}, {
|
|
||||||
name: 'path',
|
|
||||||
weight: 0.3
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
generateRoutes(routes, basePath = '/', prefixTitle = []) {
|
|
||||||
let res = []
|
|
||||||
for (const router of routes) {
|
|
||||||
if (router.hidden) { continue }
|
|
||||||
const data = {
|
|
||||||
path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
|
|
||||||
title: [...prefixTitle],
|
|
||||||
icon: ''
|
|
||||||
}
|
|
||||||
if (router.meta && router.meta.title) {
|
|
||||||
data.title = [...data.title, router.meta.title]
|
|
||||||
data.icon = router.meta.icon
|
|
||||||
if (router.redirect !== 'noRedirect') {
|
|
||||||
res.push(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (router.query) {
|
|
||||||
data.query = router.query
|
|
||||||
}
|
|
||||||
if (router.children) {
|
|
||||||
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
|
|
||||||
if (tempRoutes.length >= 1) {
|
|
||||||
res = [...res, ...tempRoutes]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
},
|
|
||||||
querySearch(query) {
|
|
||||||
this.activeIndex = -1
|
|
||||||
if (query !== '') {
|
|
||||||
const q = query.toLowerCase()
|
|
||||||
const pathMatches = this.searchPool.filter(item =>
|
|
||||||
item.path.toLowerCase().includes(q)
|
|
||||||
)
|
|
||||||
const fuseMatches = this.fuse.search(query).map(item => item.item)
|
|
||||||
const merged = [...pathMatches]
|
|
||||||
fuseMatches.forEach(item => {
|
|
||||||
if (!merged.find(m => m.path === item.path)) {
|
|
||||||
merged.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.options = merged
|
|
||||||
} else {
|
|
||||||
this.options = this.searchPool
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeStyle(index) {
|
|
||||||
if (index !== this.activeIndex) return {}
|
|
||||||
return {
|
|
||||||
'background-color': this.theme,
|
|
||||||
'color': '#fff'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigateResult(direction) {
|
|
||||||
if (direction === 'up') {
|
|
||||||
this.activeIndex = this.activeIndex <= 0 ? this.options.length - 1 : this.activeIndex - 1
|
|
||||||
} else if (direction === 'down') {
|
|
||||||
this.activeIndex = this.activeIndex >= this.options.length - 1 ? 0 : this.activeIndex + 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectActiveResult() {
|
|
||||||
if (this.options.length > 0 && this.activeIndex >= 0) {
|
|
||||||
this.change(this.options[this.activeIndex])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
highlightText(text) {
|
|
||||||
if (!text) return ''
|
|
||||||
if (!this.search) return text
|
|
||||||
const keyword = this.escapeRegExp(this.search)
|
|
||||||
const reg = new RegExp(`(${keyword})`, 'gi')
|
|
||||||
return text.replace(reg, '<span class="highlight">$1</span>')
|
|
||||||
},
|
|
||||||
escapeRegExp(str) {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDialogOpened() {
|
||||||
|
nextTick(() => {
|
||||||
|
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
|
||||||
|
search.value = ''
|
||||||
|
options.value = searchPool.value
|
||||||
|
show.value = false
|
||||||
|
activeIndex.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function change(val) {
|
||||||
|
const p = val.path
|
||||||
|
const query = val.query
|
||||||
|
if (isHttp(p)) {
|
||||||
|
// http(s):// 路径新窗口打开
|
||||||
|
const pindex = p.indexOf("http")
|
||||||
|
window.open(p.substr(pindex, p.length), "_blank")
|
||||||
|
} else {
|
||||||
|
if (query) {
|
||||||
|
router.push({ path: p, query: JSON.parse(query) })
|
||||||
|
} else {
|
||||||
|
router.push(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
search.value = ''
|
||||||
|
options.value = searchPool.value
|
||||||
|
nextTick(() => {
|
||||||
|
show.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFuse(list) {
|
||||||
|
fuse.value = new Fuse(list, {
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.2,
|
||||||
|
distance: 100,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
keys: [{
|
||||||
|
name: 'title',
|
||||||
|
weight: 0.7
|
||||||
|
}, {
|
||||||
|
name: 'path',
|
||||||
|
weight: 0.3
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRoutes(routes, basePath = '', prefixTitle = []) {
|
||||||
|
let res = []
|
||||||
|
for (const r of routes) {
|
||||||
|
if (r.hidden) { continue }
|
||||||
|
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
|
||||||
|
const data = {
|
||||||
|
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
|
||||||
|
title: [...prefixTitle],
|
||||||
|
icon: ''
|
||||||
|
}
|
||||||
|
if (r.meta && r.meta.title) {
|
||||||
|
data.title = [...data.title, r.meta.title]
|
||||||
|
data.icon = r.meta.icon
|
||||||
|
if (r.redirect !== "noRedirect") {
|
||||||
|
res.push(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r.query) {
|
||||||
|
data.query = r.query
|
||||||
|
}
|
||||||
|
if (r.children) {
|
||||||
|
const tempRoutes = generateRoutes(r.children, data.path, data.title)
|
||||||
|
if (tempRoutes.length >= 1) {
|
||||||
|
res = [...res, ...tempRoutes]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySearch(query) {
|
||||||
|
activeIndex.value = -1
|
||||||
|
if (query !== '') {
|
||||||
|
const q = query.toLowerCase()
|
||||||
|
const pathMatches = searchPool.value.filter(item =>
|
||||||
|
item.path.toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
const fuseMatches = fuse.value.search(query).map(item => item.item)
|
||||||
|
const merged = [...pathMatches]
|
||||||
|
fuseMatches.forEach(item => {
|
||||||
|
if (!merged.find(m => m.path === item.path)) {
|
||||||
|
merged.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
options.value = merged
|
||||||
|
} else {
|
||||||
|
options.value = searchPool.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeStyle(index) {
|
||||||
|
if (index !== activeIndex.value) return {}
|
||||||
|
return {
|
||||||
|
"background-color": theme.value,
|
||||||
|
"color": "#fff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateResult(direction) {
|
||||||
|
if (direction === "up") {
|
||||||
|
activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
|
||||||
|
} else if (direction === "down") {
|
||||||
|
activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectActiveResult() {
|
||||||
|
if (options.value.length > 0 && activeIndex.value >= 0) {
|
||||||
|
change(options.value[activeIndex.value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightText(text) {
|
||||||
|
if (!text) return ''
|
||||||
|
if (!search.value) return text
|
||||||
|
const keyword = escapeRegExp(search.value)
|
||||||
|
const reg = new RegExp(`(${keyword})`, 'gi')
|
||||||
|
return text.replace(reg, '<span class="highlight">$1</span>')
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(str) {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
searchPool.value = generateRoutes(routes.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(searchPool, (list) => {
|
||||||
|
initFuse(list)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
::v-deep {
|
:deep(.el-dialog__header) {
|
||||||
.el-dialog__header {
|
padding: 6px !important;
|
||||||
padding: 6px !important;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
:deep(.highlight) {
|
||||||
color: red;
|
color: red;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-active .highlight {
|
:deep(.is-active .highlight) {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search {
|
.header-search {
|
||||||
@@ -394,4 +394,4 @@ export default {
|
|||||||
box-shadow: 0 1px 0 #ccc;
|
box-shadow: 0 1px 0 #ccc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<!-- @author zhengjie -->
|
|
||||||
<template>
|
<template>
|
||||||
<div class="icon-body">
|
<div class="icon-body">
|
||||||
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
|
<el-input
|
||||||
<i slot="suffix" class="el-icon-search el-input__icon" />
|
v-model="iconName"
|
||||||
|
class="icon-search"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入图标名称"
|
||||||
|
@clear="filterIcons"
|
||||||
|
@input="filterIcons"
|
||||||
|
>
|
||||||
|
<template #suffix><i class="el-icon-search el-input__icon" /></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div class="icon-list">
|
<div class="icon-list">
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
@@ -17,42 +23,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import icons from './requireIcons'
|
import icons from './requireIcons'
|
||||||
export default {
|
|
||||||
name: 'IconSelect',
|
const props = defineProps({
|
||||||
props: {
|
activeIcon: {
|
||||||
activeIcon: {
|
type: String
|
||||||
type: String
|
}
|
||||||
}
|
})
|
||||||
},
|
|
||||||
data() {
|
const iconName = ref('')
|
||||||
return {
|
const iconList = ref(icons)
|
||||||
name: '',
|
const emit = defineEmits(['selected'])
|
||||||
iconList: icons
|
|
||||||
}
|
function filterIcons() {
|
||||||
},
|
iconList.value = icons
|
||||||
methods: {
|
if (iconName.value) {
|
||||||
filterIcons() {
|
iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
|
||||||
this.iconList = icons
|
|
||||||
if (this.name) {
|
|
||||||
this.iconList = this.iconList.filter(item => item.includes(this.name))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedIcon(name) {
|
|
||||||
this.$emit('selected', name)
|
|
||||||
document.body.click()
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.name = ''
|
|
||||||
this.iconList = icons
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedIcon(name) {
|
||||||
|
emit('selected', name)
|
||||||
|
document.body.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
iconName.value = ''
|
||||||
|
iconList.value = icons
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reset
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
.icon-body {
|
.icon-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
.icon-search {
|
.icon-search {
|
||||||
@@ -101,4 +108,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
|
let icons = []
|
||||||
|
const modules = import.meta.glob('./../../assets/icons/svg/*.svg')
|
||||||
|
for (const path in modules) {
|
||||||
|
const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
|
||||||
|
icons.push(p)
|
||||||
|
}
|
||||||
|
|
||||||
const req = require.context('../../assets/icons/svg', false, /\.svg$/)
|
export default icons
|
||||||
const requireAll = requireContext => requireContext.keys()
|
|
||||||
|
|
||||||
const re = /\.\/(.*)\.svg/
|
|
||||||
|
|
||||||
const icons = requireAll(req).map(i => {
|
|
||||||
return i.match(re)[1]
|
|
||||||
})
|
|
||||||
|
|
||||||
export default icons
|
|
||||||
@@ -4,65 +4,67 @@
|
|||||||
fit="cover"
|
fit="cover"
|
||||||
:style="`width:${realWidth};height:${realHeight};`"
|
:style="`width:${realWidth};height:${realHeight};`"
|
||||||
:preview-src-list="realSrcList"
|
:preview-src-list="realSrcList"
|
||||||
|
preview-teleported
|
||||||
>
|
>
|
||||||
<div slot="error" class="image-slot">
|
<template #error>
|
||||||
<i class="el-icon-picture-outline"></i>
|
<div class="image-slot">
|
||||||
</div>
|
<el-icon><picture-filled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-image>
|
</el-image>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { isExternal } from "@/utils/validate"
|
import { isExternal } from "@/utils/validate"
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: "ImagePreview",
|
src: {
|
||||||
props: {
|
type: String,
|
||||||
src: {
|
default: ""
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
width: {
|
||||||
realSrc() {
|
type: [Number, String],
|
||||||
if (!this.src) {
|
default: ""
|
||||||
return
|
},
|
||||||
}
|
height: {
|
||||||
let real_src = this.src.split(",")[0]
|
type: [Number, String],
|
||||||
if (isExternal(real_src)) {
|
default: ""
|
||||||
return real_src
|
|
||||||
}
|
|
||||||
return process.env.VUE_APP_BASE_API + real_src
|
|
||||||
},
|
|
||||||
realSrcList() {
|
|
||||||
if (!this.src) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let real_src_list = this.src.split(",")
|
|
||||||
let srcList = []
|
|
||||||
real_src_list.forEach(item => {
|
|
||||||
if (isExternal(item)) {
|
|
||||||
return srcList.push(item)
|
|
||||||
}
|
|
||||||
return srcList.push(process.env.VUE_APP_BASE_API + item)
|
|
||||||
})
|
|
||||||
return srcList
|
|
||||||
},
|
|
||||||
realWidth() {
|
|
||||||
return typeof this.width == "string" ? this.width : `${this.width}px`
|
|
||||||
},
|
|
||||||
realHeight() {
|
|
||||||
return typeof this.height == "string" ? this.height : `${this.height}px`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const realSrc = computed(() => {
|
||||||
|
if (!props.src) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let real_src = props.src.split(",")[0]
|
||||||
|
if (isExternal(real_src)) {
|
||||||
|
return real_src
|
||||||
|
}
|
||||||
|
return import.meta.env.VITE_APP_BASE_API + real_src
|
||||||
|
})
|
||||||
|
|
||||||
|
const realSrcList = computed(() => {
|
||||||
|
if (!props.src) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let real_src_list = props.src.split(",")
|
||||||
|
let srcList = []
|
||||||
|
real_src_list.forEach(item => {
|
||||||
|
if (isExternal(item)) {
|
||||||
|
return srcList.push(item)
|
||||||
|
}
|
||||||
|
return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
|
||||||
|
})
|
||||||
|
return srcList
|
||||||
|
})
|
||||||
|
|
||||||
|
const realWidth = computed(() =>
|
||||||
|
typeof props.width == "string" ? props.width : `${props.width}px`
|
||||||
|
)
|
||||||
|
|
||||||
|
const realHeight = computed(() =>
|
||||||
|
typeof props.height == "string" ? props.height : `${props.height}px`
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -70,14 +72,14 @@ export default {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #ebeef5;
|
background-color: #ebeef5;
|
||||||
box-shadow: 0 0 5px 1px #ccc;
|
box-shadow: 0 0 5px 1px #ccc;
|
||||||
::v-deep .el-image__inner {
|
:deep(.el-image__inner) {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
::v-deep .image-slot {
|
:deep(.image-slot) {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -12,28 +12,31 @@
|
|||||||
:on-error="handleUploadError"
|
:on-error="handleUploadError"
|
||||||
:on-exceed="handleExceed"
|
:on-exceed="handleExceed"
|
||||||
ref="imageUpload"
|
ref="imageUpload"
|
||||||
:on-remove="handleDelete"
|
:before-remove="handleDelete"
|
||||||
:show-file-list="true"
|
:show-file-list="true"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
:on-preview="handlePictureCardPreview"
|
:on-preview="handlePictureCardPreview"
|
||||||
:class="{hide: this.fileList.length >= this.limit}"
|
:class="{ hide: fileList.length >= limit }"
|
||||||
>
|
>
|
||||||
<i class="el-icon-plus"></i>
|
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
|
||||||
<!-- 上传提示 -->
|
<!-- 上传提示 -->
|
||||||
<div class="el-upload__tip" slot="tip" v-if="showTip && !disabled">
|
<div class="el-upload__tip" v-if="showTip && !disabled">
|
||||||
请上传
|
请上传
|
||||||
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
|
<template v-if="fileSize">
|
||||||
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
|
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||||
|
</template>
|
||||||
|
<template v-if="fileType">
|
||||||
|
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
|
||||||
|
</template>
|
||||||
的文件
|
的文件
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible.sync="dialogVisible"
|
v-model="dialogVisible"
|
||||||
title="预览"
|
title="预览"
|
||||||
width="800"
|
width="800px"
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -44,229 +47,212 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { getToken } from "@/utils/auth"
|
import { getToken } from "@/utils/auth"
|
||||||
import { isExternal } from "@/utils/validate"
|
import { isExternal } from "@/utils/validate"
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
modelValue: [String, Object, Array],
|
||||||
value: [String, Object, Array],
|
// 上传接口地址
|
||||||
// 上传接口地址
|
action: {
|
||||||
action: {
|
type: String,
|
||||||
type: String,
|
default: "/common/upload"
|
||||||
default: "/common/upload"
|
|
||||||
},
|
|
||||||
// 上传携带的参数
|
|
||||||
data: {
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
// 图片数量限制
|
|
||||||
limit: {
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
// 大小限制(MB)
|
|
||||||
fileSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
|
||||||
fileType: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ["png", "jpg", "jpeg"]
|
|
||||||
},
|
|
||||||
// 是否显示提示
|
|
||||||
isShowTip: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
// 禁用组件(仅查看图片)
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 拖动排序
|
|
||||||
drag: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
// 上传携带的参数
|
||||||
return {
|
data: {
|
||||||
number: 0,
|
type: Object
|
||||||
uploadList: [],
|
|
||||||
dialogImageUrl: "",
|
|
||||||
dialogVisible: false,
|
|
||||||
hideUpload: false,
|
|
||||||
baseUrl: process.env.VUE_APP_BASE_API,
|
|
||||||
uploadImgUrl: process.env.VUE_APP_BASE_API + this.action, // 上传的图片服务器地址
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + getToken(),
|
|
||||||
},
|
|
||||||
fileList: []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
// 图片数量限制
|
||||||
if (this.drag && !this.disabled) {
|
limit: {
|
||||||
this.$nextTick(() => {
|
type: Number,
|
||||||
const element = this.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
|
default: 5
|
||||||
Sortable.create(element, {
|
|
||||||
onEnd: (evt) => {
|
|
||||||
const movedItem = this.fileList.splice(evt.oldIndex, 1)[0]
|
|
||||||
this.fileList.splice(evt.newIndex, 0, movedItem)
|
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
// 大小限制(MB)
|
||||||
value: {
|
fileSize: {
|
||||||
handler(val) {
|
type: Number,
|
||||||
if (val) {
|
default: 5
|
||||||
// 首先将值转为数组
|
|
||||||
const list = Array.isArray(val) ? val : this.value.split(',')
|
|
||||||
// 然后将数组转为对象数组
|
|
||||||
this.fileList = list.map(item => {
|
|
||||||
if (typeof item === "string") {
|
|
||||||
if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) {
|
|
||||||
item = { name: this.baseUrl + item, url: this.baseUrl + item }
|
|
||||||
} else {
|
|
||||||
item = { name: item, url: item }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.fileList = []
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||||
// 是否显示提示
|
fileType: {
|
||||||
showTip() {
|
type: Array,
|
||||||
return this.isShowTip && (this.fileType || this.fileSize)
|
default: () => ["png", "jpg", "jpeg"]
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
// 是否显示提示
|
||||||
// 上传前loading加载
|
isShowTip: {
|
||||||
handleBeforeUpload(file) {
|
type: Boolean,
|
||||||
let isImg = false
|
default: true
|
||||||
if (this.fileType.length) {
|
},
|
||||||
let fileExtension = ""
|
// 禁用组件(仅查看图片)
|
||||||
if (file.name.lastIndexOf(".") > -1) {
|
disabled: {
|
||||||
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
|
type: Boolean,
|
||||||
}
|
default: false
|
||||||
isImg = this.fileType.some(type => {
|
},
|
||||||
if (file.type.indexOf(type) > -1) return true
|
// 拖动排序
|
||||||
if (fileExtension && fileExtension.indexOf(type) > -1) return true
|
drag: {
|
||||||
return false
|
type: Boolean,
|
||||||
})
|
default: true
|
||||||
} else {
|
}
|
||||||
isImg = file.type.indexOf("image") > -1
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (!isImg) {
|
const { proxy } = getCurrentInstance()
|
||||||
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}图片格式文件!`)
|
const emit = defineEmits()
|
||||||
return false
|
const number = ref(0)
|
||||||
}
|
const uploadList = ref([])
|
||||||
if (file.name.includes(',')) {
|
const dialogImageUrl = ref("")
|
||||||
this.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
const dialogVisible = ref(false)
|
||||||
return false
|
const baseUrl = import.meta.env.VITE_APP_BASE_API
|
||||||
}
|
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
|
||||||
if (this.fileSize) {
|
const headers = ref({ Authorization: "Bearer " + getToken() })
|
||||||
const isLt = file.size / 1024 / 1024 < this.fileSize
|
const fileList = ref([])
|
||||||
if (!isLt) {
|
const showTip = computed(
|
||||||
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`)
|
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||||
return false
|
)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, val => {
|
||||||
|
if (val) {
|
||||||
|
// 首先将值转为数组
|
||||||
|
const list = Array.isArray(val) ? val : props.modelValue.split(",")
|
||||||
|
// 然后将数组转为对象数组
|
||||||
|
fileList.value = list.map(item => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
|
||||||
|
item = { name: baseUrl + item, url: baseUrl + item }
|
||||||
|
} else {
|
||||||
|
item = { name: item, url: item }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$modal.loading("正在上传图片,请稍候...")
|
return item
|
||||||
this.number++
|
})
|
||||||
},
|
} else {
|
||||||
// 文件个数超出
|
fileList.value = []
|
||||||
handleExceed() {
|
return []
|
||||||
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`)
|
}
|
||||||
},
|
},{ deep: true, immediate: true })
|
||||||
// 上传成功回调
|
|
||||||
handleUploadSuccess(res, file) {
|
// 上传前loading加载
|
||||||
if (res.code === 200) {
|
function handleBeforeUpload(file) {
|
||||||
this.uploadList.push({ name: res.fileName, url: res.fileName })
|
let isImg = false
|
||||||
this.uploadedSuccessfully()
|
if (props.fileType.length) {
|
||||||
} else {
|
let fileExtension = ""
|
||||||
this.number--
|
if (file.name.lastIndexOf(".") > -1) {
|
||||||
this.$modal.closeLoading()
|
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
|
||||||
this.$modal.msgError(res.msg)
|
}
|
||||||
this.$refs.imageUpload.handleRemove(file)
|
isImg = props.fileType.some(type => {
|
||||||
this.uploadedSuccessfully()
|
if (file.type.indexOf(type) > -1) return true
|
||||||
}
|
if (fileExtension && fileExtension.indexOf(type) > -1) return true
|
||||||
},
|
return false
|
||||||
// 删除图片
|
})
|
||||||
handleDelete(file) {
|
} else {
|
||||||
const findex = this.fileList.map(f => f.name).indexOf(file.name)
|
isImg = file.type.indexOf("image") > -1
|
||||||
if (findex > -1) {
|
}
|
||||||
this.fileList.splice(findex, 1)
|
if (!isImg) {
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
|
||||||
}
|
return false
|
||||||
},
|
}
|
||||||
// 上传失败
|
if (file.name.includes(',')) {
|
||||||
handleUploadError() {
|
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
||||||
this.$modal.msgError("上传图片失败,请重试")
|
return false
|
||||||
this.$modal.closeLoading()
|
}
|
||||||
},
|
if (props.fileSize) {
|
||||||
// 上传结束处理
|
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||||
uploadedSuccessfully() {
|
if (!isLt) {
|
||||||
if (this.number > 0 && this.uploadList.length === this.number) {
|
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
|
||||||
this.fileList = this.fileList.concat(this.uploadList)
|
return false
|
||||||
this.uploadList = []
|
|
||||||
this.number = 0
|
|
||||||
this.$emit("input", this.listToString(this.fileList))
|
|
||||||
this.$modal.closeLoading()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 预览
|
|
||||||
handlePictureCardPreview(file) {
|
|
||||||
this.dialogImageUrl = file.url
|
|
||||||
this.dialogVisible = true
|
|
||||||
},
|
|
||||||
// 对象转成指定字符串分隔
|
|
||||||
listToString(list, separator) {
|
|
||||||
let strs = ""
|
|
||||||
separator = separator || ","
|
|
||||||
for (let i in list) {
|
|
||||||
if (list[i].url) {
|
|
||||||
strs += list[i].url.replace(this.baseUrl, "") + separator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strs != '' ? strs.substr(0, strs.length - 1) : ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
proxy.$modal.loading("正在上传图片,请稍候...")
|
||||||
|
number.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文件个数超出
|
||||||
|
function handleExceed() {
|
||||||
|
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功回调
|
||||||
|
function handleUploadSuccess(res, file) {
|
||||||
|
if (res.code === 200) {
|
||||||
|
uploadList.value.push({ name: res.fileName, url: res.fileName })
|
||||||
|
uploadedSuccessfully()
|
||||||
|
} else {
|
||||||
|
number.value--
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
proxy.$modal.msgError(res.msg)
|
||||||
|
proxy.$refs.imageUpload.handleRemove(file)
|
||||||
|
uploadedSuccessfully()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除图片
|
||||||
|
function handleDelete(file) {
|
||||||
|
const findex = fileList.value.map(f => f.name).indexOf(file.name)
|
||||||
|
if (findex > -1 && uploadList.value.length === number.value) {
|
||||||
|
fileList.value.splice(findex, 1)
|
||||||
|
emit("update:modelValue", listToString(fileList.value))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传结束处理
|
||||||
|
function uploadedSuccessfully() {
|
||||||
|
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||||
|
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
|
||||||
|
uploadList.value = []
|
||||||
|
number.value = 0
|
||||||
|
emit("update:modelValue", listToString(fileList.value))
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传失败
|
||||||
|
function handleUploadError() {
|
||||||
|
proxy.$modal.msgError("上传图片失败")
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览
|
||||||
|
function handlePictureCardPreview(file) {
|
||||||
|
dialogImageUrl.value = file.url
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对象转成指定字符串分隔
|
||||||
|
function listToString(list, separator) {
|
||||||
|
let strs = ""
|
||||||
|
separator = separator || ","
|
||||||
|
for (let i in list) {
|
||||||
|
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
|
||||||
|
strs += list[i].url.replace(baseUrl, "") + separator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strs != "" ? strs.substr(0, strs.length - 1) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化拖拽排序
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.drag && !props.disabled) {
|
||||||
|
nextTick(() => {
|
||||||
|
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
|
||||||
|
Sortable.create(element, {
|
||||||
|
onEnd: (evt) => {
|
||||||
|
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
|
||||||
|
fileList.value.splice(evt.newIndex, 0, movedItem)
|
||||||
|
emit('update:modelValue', listToString(fileList.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// .el-upload--picture-card 控制加号部分
|
// .el-upload--picture-card 控制加号部分
|
||||||
::v-deep.hide .el-upload--picture-card {
|
:deep(.hide .el-upload--picture-card) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-upload-list--picture-card.is-disabled + .el-upload--picture-card {
|
:deep(.el-upload.el-upload--picture-card.is-disabled) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
// 去掉动画效果
|
|
||||||
::v-deep .el-list-enter-active,
|
|
||||||
::v-deep .el-list-leave-active {
|
|
||||||
transition: all 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-list-enter, .el-list-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,104 +1,96 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{'hidden':hidden}" class="pagination-container">
|
<div :class="{ 'hidden': hidden }" class="pagination-container">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
:background="background"
|
:background="background"
|
||||||
:current-page.sync="currentPage"
|
v-model:current-page="currentPage"
|
||||||
:page-size.sync="pageSize"
|
v-model:page-size="pageSize"
|
||||||
:layout="layout"
|
:layout="layout"
|
||||||
:page-sizes="pageSizes"
|
:page-sizes="pageSizes"
|
||||||
:pager-count="pagerCount"
|
:pager-count="pagerCount"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-bind="$attrs"
|
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { scrollTo } from '@/utils/scroll-to'
|
import { scrollTo } from '@/utils/scroll-to'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'Pagination',
|
total: {
|
||||||
props: {
|
required: true,
|
||||||
total: {
|
type: Number
|
||||||
required: true,
|
},
|
||||||
type: Number
|
page: {
|
||||||
},
|
type: Number,
|
||||||
page: {
|
default: 1
|
||||||
type: Number,
|
},
|
||||||
default: 1
|
limit: {
|
||||||
},
|
type: Number,
|
||||||
limit: {
|
default: 20
|
||||||
type: Number,
|
},
|
||||||
default: 20
|
pageSizes: {
|
||||||
},
|
type: Array,
|
||||||
pageSizes: {
|
default() {
|
||||||
type: Array,
|
return [10, 20, 30, 50]
|
||||||
default() {
|
|
||||||
return [10, 20, 30, 50]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 移动端页码按钮的数量端默认值5
|
|
||||||
pagerCount: {
|
|
||||||
type: Number,
|
|
||||||
default: document.body.clientWidth < 992 ? 5 : 7
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
type: String,
|
|
||||||
default: 'total, sizes, prev, pager, next, jumper'
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
autoScroll: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
hidden: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
// 移动端页码按钮的数量端默认值5
|
||||||
return {
|
pagerCount: {
|
||||||
}
|
type: Number,
|
||||||
|
default: document.body.clientWidth < 992 ? 5 : 7
|
||||||
},
|
},
|
||||||
computed: {
|
layout: {
|
||||||
currentPage: {
|
type: String,
|
||||||
get() {
|
default: 'total, sizes, prev, pager, next, jumper'
|
||||||
return this.page
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$emit('update:page', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pageSize: {
|
|
||||||
get() {
|
|
||||||
return this.limit
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$emit('update:limit', val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
background: {
|
||||||
handleSizeChange(val) {
|
type: Boolean,
|
||||||
if (this.currentPage * val > this.total) {
|
default: true
|
||||||
this.currentPage = 1
|
},
|
||||||
}
|
autoScroll: {
|
||||||
this.$emit('pagination', { page: this.currentPage, limit: val })
|
type: Boolean,
|
||||||
if (this.autoScroll) {
|
default: true
|
||||||
scrollTo(0, 800)
|
},
|
||||||
}
|
hidden: {
|
||||||
},
|
type: Boolean,
|
||||||
handleCurrentChange(val) {
|
default: false
|
||||||
this.$emit('pagination', { page: val, limit: this.pageSize })
|
}
|
||||||
if (this.autoScroll) {
|
})
|
||||||
scrollTo(0, 800)
|
|
||||||
}
|
const emit = defineEmits()
|
||||||
}
|
const currentPage = computed({
|
||||||
|
get() {
|
||||||
|
return props.page
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:page', val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const pageSize = computed({
|
||||||
|
get() {
|
||||||
|
return props.limit
|
||||||
|
},
|
||||||
|
set(val){
|
||||||
|
emit('update:limit', val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSizeChange(val) {
|
||||||
|
if (currentPage.value * val > props.total) {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
emit('pagination', { page: currentPage.value, limit: val })
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val) {
|
||||||
|
emit('pagination', { page: val, limit: pageSize.value })
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -110,4 +102,4 @@ export default {
|
|||||||
.pagination-container.hidden {
|
.pagination-container.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
|
|
||||||
<div class="pan-info">
|
|
||||||
<div class="pan-info-roles-container">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'PanThumb',
|
|
||||||
props: {
|
|
||||||
image: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
zIndex: {
|
|
||||||
type: Number,
|
|
||||||
default: 1
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: String,
|
|
||||||
default: '150px'
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: String,
|
|
||||||
default: '150px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pan-item {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
cursor: default;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-info-roles-container {
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-position: center center;
|
|
||||||
background-size: cover;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
transform-origin: 95% 40%;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .pan-thumb:after {
|
|
||||||
content: '';
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 40%;
|
|
||||||
left: 95%;
|
|
||||||
margin: -4px 0 0 -4px;
|
|
||||||
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
|
|
||||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
|
|
||||||
} */
|
|
||||||
|
|
||||||
.pan-info {
|
|
||||||
position: absolute;
|
|
||||||
width: inherit;
|
|
||||||
height: inherit;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-info h3 {
|
|
||||||
color: #fff;
|
|
||||||
text-transform: uppercase;
|
|
||||||
position: relative;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
font-size: 18px;
|
|
||||||
margin: 0 60px;
|
|
||||||
padding: 22px 0 0 0;
|
|
||||||
height: 85px;
|
|
||||||
font-family: 'Open Sans', Arial, sans-serif;
|
|
||||||
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-info p {
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px 5px;
|
|
||||||
font-style: italic;
|
|
||||||
margin: 0 30px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-info p a {
|
|
||||||
display: block;
|
|
||||||
color: #333;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #fff;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 9px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
padding-top: 24px;
|
|
||||||
margin: 7px auto 0;
|
|
||||||
font-family: 'Open Sans', Arial, sans-serif;
|
|
||||||
opacity: 0;
|
|
||||||
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
|
|
||||||
transform: translateX(60px) rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-info p a:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-item:hover .pan-thumb {
|
|
||||||
transform: rotate(-110deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pan-item:hover .pan-info p a {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0px) rotate(0deg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<svg-icon icon-class="question" @click="goto" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'RuoYiDoc',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
url: 'http://doc.ruoyi.vip/ruoyi-vue'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
goto() {
|
|
||||||
window.open(this.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<svg-icon icon-class="github" @click="goto" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'RuoYiGit',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
url: 'https://gitee.com/y_project/RuoYi-Vue'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
goto() {
|
|
||||||
window.open(this.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -2,31 +2,33 @@
|
|||||||
<div class="top-right-btn" :style="style">
|
<div class="top-right-btn" :style="style">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
|
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
|
||||||
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" />
|
<el-button circle icon="Search" @click="toggleSearch()" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
|
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
|
||||||
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
|
<el-button circle icon="Refresh" @click="refresh()" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
|
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
|
||||||
<el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
|
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
|
||||||
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
|
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
|
||||||
<el-button size="mini" circle icon="el-icon-menu" />
|
<el-button circle icon="Menu" />
|
||||||
<el-dropdown-menu slot="dropdown">
|
<template #dropdown>
|
||||||
<!-- 全选/反选 按钮 -->
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item>
|
<!-- 全选/反选 按钮 -->
|
||||||
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
|
<el-dropdown-item>
|
||||||
</el-dropdown-item>
|
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
|
||||||
<div class="check-line"></div>
|
|
||||||
<template v-for="(item, key) in columns">
|
|
||||||
<el-dropdown-item :key="key">
|
|
||||||
<el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
|
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</template>
|
<div class="check-line"></div>
|
||||||
</el-dropdown-menu>
|
<template v-for="(item, key) in columns" :key="item.key">
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
|
||||||
|
</el-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-dialog :title="title" :visible.sync="open" append-to-body>
|
<el-dialog :title="title" v-model="open" append-to-body>
|
||||||
<el-transfer
|
<el-transfer
|
||||||
:titles="['显示', '隐藏']"
|
:titles="['显示', '隐藏']"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
@@ -37,213 +39,207 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import cache from '@/plugins/cache'
|
import cache from '@/plugins/cache'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: "RightToolbar",
|
/* 是否显示检索条件 */
|
||||||
data() {
|
showSearch: {
|
||||||
return {
|
type: Boolean,
|
||||||
// 显隐数据
|
default: true
|
||||||
value: [],
|
|
||||||
// 弹出层标题
|
|
||||||
title: "显示/隐藏",
|
|
||||||
// 是否显示弹出层
|
|
||||||
open: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
props: {
|
/* 显隐列信息(数组格式、对象格式) */
|
||||||
/* 是否显示检索条件 */
|
columns: {
|
||||||
showSearch: {
|
type: [Array, Object],
|
||||||
type: Boolean,
|
default: () => ({})
|
||||||
default: true
|
|
||||||
},
|
|
||||||
/* 显隐列信息(数组格式、对象格式) */
|
|
||||||
columns: {
|
|
||||||
type: [Array, Object],
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
/* 是否显示检索图标 */
|
|
||||||
search: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
|
|
||||||
showColumnsType: {
|
|
||||||
type: String,
|
|
||||||
default: "checkbox"
|
|
||||||
},
|
|
||||||
/* 右外边距 */
|
|
||||||
gutter: {
|
|
||||||
type: Number,
|
|
||||||
default: 10
|
|
||||||
},
|
|
||||||
/* 列显隐状态记忆的 localStorage key(传入则启用记忆,不传则不记忆) */
|
|
||||||
storageKey: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
/* 是否显示检索图标 */
|
||||||
style() {
|
search: {
|
||||||
const ret = {}
|
type: Boolean,
|
||||||
if (this.gutter) {
|
default: true
|
||||||
ret.marginRight = `${this.gutter / 2}px`
|
},
|
||||||
}
|
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
|
||||||
return ret
|
showColumnsType: {
|
||||||
},
|
type: String,
|
||||||
isChecked: {
|
default: "checkbox"
|
||||||
get() {
|
},
|
||||||
return Array.isArray(this.columns) ? this.columns.every((col) => col.visible) : Object.values(this.columns).every((col) => col.visible)
|
/* 右外边距 */
|
||||||
},
|
gutter: {
|
||||||
set() {}
|
type: Number,
|
||||||
},
|
default: 10
|
||||||
isIndeterminate() {
|
},
|
||||||
return Array.isArray(this.columns) ? this.columns.some((col) => col.visible) && !this.isChecked : Object.values(this.columns).some((col) => col.visible) && !this.isChecked
|
/* 列显隐状态记忆的 localStorage key(传入则启用记忆,不传则不记忆) */
|
||||||
},
|
storageKey: {
|
||||||
transferData() {
|
type: String,
|
||||||
if (Array.isArray(this.columns)) {
|
default: ""
|
||||||
return this.columns.map((item, index) => ({ key: index, label: item.label }))
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:showSearch', 'queryTable'])
|
||||||
|
|
||||||
|
// 显隐数据
|
||||||
|
const value = ref([])
|
||||||
|
// 弹出层标题
|
||||||
|
const title = ref("显示/隐藏")
|
||||||
|
// 是否显示弹出层
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
const ret = {}
|
||||||
|
if (props.gutter) {
|
||||||
|
ret.marginRight = `${props.gutter / 2}px`
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否全选/半选 状态
|
||||||
|
const isChecked = computed({
|
||||||
|
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
|
||||||
|
set: () => {}
|
||||||
|
})
|
||||||
|
const isIndeterminate = computed(() => Array.isArray(props.columns) ? props.columns.some((col) => col.visible) && !isChecked.value : Object.values(props.columns).some((col) => col.visible) && !isChecked.value)
|
||||||
|
const transferData = computed(() => Array.isArray(props.columns) ? props.columns.map((item, index) => ({ key: index, label: item.label })) : Object.keys(props.columns).map((key, index) => ({ key: index, label: props.columns[key].label })))
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
function toggleSearch() {
|
||||||
|
let el = proxy.$el
|
||||||
|
let formEl = null
|
||||||
|
while ((el = el.parentElement) && el !== document.body) {
|
||||||
|
if ((formEl = el.querySelector('.el-form'))) break
|
||||||
|
}
|
||||||
|
if (!formEl) return emits('update:showSearch', !props.showSearch)
|
||||||
|
animateSearch(formEl, props.showSearch)
|
||||||
|
}
|
||||||
|
function animateSearch(el, isHide) {
|
||||||
|
const DURATION = 260
|
||||||
|
const TRANSITION = 'max-height 0.25s ease, opacity 0.2s ease'
|
||||||
|
const clear = () => Object.assign(el.style, { transition: '', maxHeight: '', opacity: '', overflow: '' })
|
||||||
|
Object.assign(el.style, { overflow: 'hidden', transition: '' })
|
||||||
|
if (isHide) {
|
||||||
|
Object.assign(el.style, { maxHeight: el.scrollHeight + 'px', opacity: '1', transition: TRANSITION })
|
||||||
|
requestAnimationFrame(() => Object.assign(el.style, { maxHeight: '0', opacity: '0' }))
|
||||||
|
setTimeout(() => { emits('update:showSearch', false); clear() }, DURATION)
|
||||||
|
} else {
|
||||||
|
emits('update:showSearch', true)
|
||||||
|
nextTick(() => {
|
||||||
|
Object.assign(el.style, { maxHeight: '0', opacity: '0' })
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
|
Object.assign(el.style, { transition: TRANSITION, maxHeight: el.scrollHeight + 'px', opacity: '1' })
|
||||||
|
}))
|
||||||
|
setTimeout(clear, DURATION)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
function refresh() {
|
||||||
|
emits("queryTable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧列表元素变化
|
||||||
|
function dataChange(data) {
|
||||||
|
if (Array.isArray(props.columns)) {
|
||||||
|
for (let item in props.columns) {
|
||||||
|
const key = props.columns[item].key
|
||||||
|
props.columns[item].visible = !data.includes(key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.keys(props.columns).forEach((key, index) => {
|
||||||
|
props.columns[key].visible = !data.includes(index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
saveStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开显隐列dialog
|
||||||
|
function showColumn() {
|
||||||
|
open.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果传入了 storageKey,从 localStorage 恢复列显隐状态
|
||||||
|
if (props.storageKey) {
|
||||||
|
try {
|
||||||
|
const saved = cache.local.getJSON(props.storageKey)
|
||||||
|
if (saved && typeof saved === 'object') {
|
||||||
|
if (Array.isArray(props.columns)) {
|
||||||
|
props.columns.forEach((col, index) => {
|
||||||
|
if (saved[index] !== undefined) col.visible = saved[index]
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return Object.keys(this.columns).map((key, index) => ({ key: index, label: this.columns[key].label }))
|
Object.keys(props.columns).forEach(key => {
|
||||||
}
|
if (saved[key] !== undefined) props.columns[key].visible = saved[key]
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
// 如果传入了 storageKey,从 localStorage 恢复列显隐状态
|
|
||||||
if (this.storageKey) {
|
|
||||||
try {
|
|
||||||
const saved = cache.local.getJSON(this.storageKey)
|
|
||||||
if (saved && typeof saved === 'object') {
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
this.columns.forEach((col, index) => {
|
|
||||||
if (saved[index] !== undefined) col.visible = saved[index]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Object.keys(this.columns).forEach(key => {
|
|
||||||
if (saved[key] !== undefined) this.columns[key].visible = saved[key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
if (this.showColumnsType == 'transfer') {
|
|
||||||
// transfer穿梭显隐列初始默认隐藏列
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
for (let item in this.columns) {
|
|
||||||
if (this.columns[item].visible === false) {
|
|
||||||
this.value.push(parseInt(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Object.keys(this.columns).forEach((key, index) => {
|
|
||||||
if (this.columns[key].visible === false) {
|
|
||||||
this.value.push(index)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
} catch (e) {}
|
||||||
methods: {
|
}
|
||||||
// 搜索
|
if (props.showColumnsType == "transfer") {
|
||||||
toggleSearch() {
|
// transfer穿梭显隐列初始默认隐藏列
|
||||||
let el = this.$el
|
if (Array.isArray(props.columns)) {
|
||||||
let formEl = null
|
for (let item in props.columns) {
|
||||||
while ((el = el.parentElement) && el !== document.body) {
|
if (props.columns[item].visible === false) {
|
||||||
if ((formEl = el.querySelector('.el-form'))) break
|
value.value.push(parseInt(item))
|
||||||
}
|
}
|
||||||
if (!formEl) return this.$emit('update:showSearch', !this.showSearch)
|
|
||||||
this._animateSearch(formEl, this.showSearch)
|
|
||||||
},
|
|
||||||
// 搜索栏动画
|
|
||||||
_animateSearch(el, isHide) {
|
|
||||||
const DURATION = 260
|
|
||||||
const TRANSITION = 'max-height 0.25s ease, opacity 0.2s ease'
|
|
||||||
const clear = () => Object.assign(el.style, { transition: '', maxHeight: '', opacity: '', overflow: '' })
|
|
||||||
Object.assign(el.style, { overflow: 'hidden', transition: '' })
|
|
||||||
if (isHide) {
|
|
||||||
Object.assign(el.style, { maxHeight: el.scrollHeight + 'px', opacity: '1', transition: TRANSITION })
|
|
||||||
requestAnimationFrame(() => Object.assign(el.style, { maxHeight: '0', opacity: '0' }))
|
|
||||||
setTimeout(() => { this.$emit('update:showSearch', false); clear() }, DURATION)
|
|
||||||
} else {
|
|
||||||
this.$emit('update:showSearch', true)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
Object.assign(el.style, { maxHeight: '0', opacity: '0' })
|
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
||||||
Object.assign(el.style, { transition: TRANSITION, maxHeight: el.scrollHeight + 'px', opacity: '1' })
|
|
||||||
}))
|
|
||||||
setTimeout(clear, DURATION)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 刷新
|
|
||||||
refresh() {
|
|
||||||
this.$emit("queryTable")
|
|
||||||
},
|
|
||||||
// 右侧列表元素变化
|
|
||||||
dataChange(data) {
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
for (let item in this.columns) {
|
|
||||||
const key = this.columns[item].key
|
|
||||||
this.columns[item].visible = !data.includes(key)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Object.keys(this.columns).forEach((key, index) => {
|
|
||||||
this.columns[key].visible = !data.includes(index)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.saveStorage()
|
|
||||||
},
|
|
||||||
// 打开显隐列dialog
|
|
||||||
showColumn() {
|
|
||||||
this.open = true
|
|
||||||
},
|
|
||||||
// 单勾选
|
|
||||||
checkboxChange(event, key) {
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
this.columns.filter(item => item.key == key)[0].visible = event
|
|
||||||
} else {
|
|
||||||
this.columns[key].visible = event
|
|
||||||
}
|
|
||||||
this.saveStorage()
|
|
||||||
},
|
|
||||||
// 切换全选/反选
|
|
||||||
toggleCheckAll() {
|
|
||||||
const newValue = !this.isChecked
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
this.columns.forEach((col) => (col.visible = newValue))
|
|
||||||
} else {
|
|
||||||
Object.values(this.columns).forEach((col) => (col.visible = newValue))
|
|
||||||
}
|
|
||||||
this.saveStorage()
|
|
||||||
},
|
|
||||||
// 将当前列显隐状态持久化到 localStorage
|
|
||||||
saveStorage() {
|
|
||||||
if (!this.storageKey) return
|
|
||||||
try {
|
|
||||||
let state = {}
|
|
||||||
if (Array.isArray(this.columns)) {
|
|
||||||
this.columns.forEach((col, index) => { state[index] = col.visible })
|
|
||||||
} else {
|
|
||||||
Object.keys(this.columns).forEach(key => { state[key] = this.columns[key].visible })
|
|
||||||
}
|
|
||||||
cache.local.setJSON(this.storageKey, state)
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
|
Object.keys(props.columns).forEach((key, index) => {
|
||||||
|
if (props.columns[key].visible === false) {
|
||||||
|
value.value.push(index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单勾选
|
||||||
|
function checkboxChange(event, key) {
|
||||||
|
if (Array.isArray(props.columns)) {
|
||||||
|
props.columns.filter(item => item.key == key)[0].visible = event
|
||||||
|
} else {
|
||||||
|
props.columns[key].visible = event
|
||||||
|
}
|
||||||
|
saveStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换全选/反选
|
||||||
|
function toggleCheckAll() {
|
||||||
|
const newValue = !isChecked.value
|
||||||
|
if (Array.isArray(props.columns)) {
|
||||||
|
props.columns.forEach((col) => (col.visible = newValue))
|
||||||
|
} else {
|
||||||
|
Object.values(props.columns).forEach((col) => (col.visible = newValue))
|
||||||
|
}
|
||||||
|
saveStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将当前列显隐状态持久化到 localStorage
|
||||||
|
function saveStorage() {
|
||||||
|
if (!props.storageKey) return
|
||||||
|
try {
|
||||||
|
let state = {}
|
||||||
|
if (Array.isArray(props.columns)) {
|
||||||
|
props.columns.forEach((col, index) => { state[index] = col.visible })
|
||||||
|
} else {
|
||||||
|
Object.keys(props.columns).forEach(key => { state[key] = props.columns[key].visible })
|
||||||
|
}
|
||||||
|
cache.local.setJSON(props.storageKey, state)
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
::v-deep .el-transfer__button {
|
:deep(.el-transfer__button) {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 12px;
|
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
::v-deep .el-transfer__button:first-child {
|
:deep(.el-transfer__button:first-child) {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
:deep(.el-dropdown-menu__item) {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 17px;
|
||||||
|
}
|
||||||
.check-line {
|
.check-line {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<svg-icon icon-class="question" @click="goto" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const url = ref('http://doc.ruoyi.vip/ruoyi-vue')
|
||||||
|
|
||||||
|
function goto() {
|
||||||
|
window.open(url.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<svg-icon icon-class="github" @click="goto" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const url = ref('https://gitee.com/y_project/RuoYi-Vue')
|
||||||
|
|
||||||
|
function goto() {
|
||||||
|
window.open(url.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,57 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
|
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import screenfull from 'screenfull'
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
|
||||||
export default {
|
const { isFullscreen, toggle } = useFullscreen()
|
||||||
name: 'Screenfull',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isFullscreen: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.destroy()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
click() {
|
|
||||||
if (!screenfull.isEnabled) {
|
|
||||||
this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
screenfull.toggle()
|
|
||||||
},
|
|
||||||
change() {
|
|
||||||
this.isFullscreen = screenfull.isFullscreen
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
if (screenfull.isEnabled) {
|
|
||||||
screenfull.on('change', this.change)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroy() {
|
|
||||||
if (screenfull.isEnabled) {
|
|
||||||
screenfull.off('change', this.change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang='scss' scoped>
|
||||||
.screenfull-svg {
|
.screenfull-svg {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: #5a5e66;;
|
fill: #5a5e66;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
vertical-align: 10px;
|
vertical-align: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,55 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dropdown trigger="click" @command="handleSetSize">
|
<div>
|
||||||
<div>
|
<el-dropdown trigger="click" @command="handleSetSize">
|
||||||
<svg-icon class-name="size-icon" icon-class="size" />
|
<div class="size-icon--style">
|
||||||
</div>
|
<svg-icon class-name="size-icon" icon-class="size" />
|
||||||
<el-dropdown-menu slot="dropdown">
|
</div>
|
||||||
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
|
<template #dropdown>
|
||||||
{{ item.label }}
|
<el-dropdown-menu>
|
||||||
</el-dropdown-item>
|
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
|
||||||
</el-dropdown-menu>
|
{{ item.label }}
|
||||||
</el-dropdown>
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import useAppStore from "@/store/modules/app"
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sizeOptions: [
|
|
||||||
{ label: 'Default', value: 'default' },
|
|
||||||
{ label: 'Medium', value: 'medium' },
|
|
||||||
{ label: 'Small', value: 'small' },
|
|
||||||
{ label: 'Mini', value: 'mini' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
size() {
|
|
||||||
return this.$store.getters.size
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleSetSize(size) {
|
|
||||||
this.$ELEMENT.size = size
|
|
||||||
this.$store.dispatch('app/setSize', size)
|
|
||||||
this.refreshView()
|
|
||||||
this.$message({
|
|
||||||
message: 'Switch Size Success',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
refreshView() {
|
|
||||||
// In order to make the cached page re-rendered
|
|
||||||
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
|
|
||||||
|
|
||||||
const { fullPath } = this.$route
|
const appStore = useAppStore()
|
||||||
|
const size = computed(() => appStore.size)
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const sizeOptions = ref([
|
||||||
|
{ label: "较大", value: "large" },
|
||||||
|
{ label: "默认", value: "default" },
|
||||||
|
{ label: "稍小", value: "small" },
|
||||||
|
])
|
||||||
|
|
||||||
this.$nextTick(() => {
|
function handleSetSize(size) {
|
||||||
this.$router.replace({
|
proxy.$modal.loading("正在设置布局大小,请稍候...")
|
||||||
path: '/redirect' + fullPath
|
appStore.setSize(size)
|
||||||
})
|
setTimeout("window.location.reload()", 1000)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.size-icon--style {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 50px;
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
|
<svg :class="svgClass" aria-hidden="true">
|
||||||
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
|
<use :xlink:href="iconName" :fill="color" />
|
||||||
<use :xlink:href="iconName" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isExternal } from '@/utils/validate'
|
export default defineComponent({
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SvgIcon',
|
|
||||||
props: {
|
props: {
|
||||||
iconClass: {
|
iconClass: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -18,44 +14,40 @@ export default {
|
|||||||
className: {
|
className: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
setup(props) {
|
||||||
isExternal() {
|
return {
|
||||||
return isExternal(this.iconClass)
|
iconName: computed(() => `#icon-${props.iconClass}`),
|
||||||
},
|
svgClass: computed(() => {
|
||||||
iconName() {
|
if (props.className) {
|
||||||
return `#icon-${this.iconClass}`
|
return `svg-icon ${props.className}`
|
||||||
},
|
}
|
||||||
svgClass() {
|
|
||||||
if (this.className) {
|
|
||||||
return 'svg-icon ' + this.className
|
|
||||||
} else {
|
|
||||||
return 'svg-icon'
|
return 'svg-icon'
|
||||||
}
|
})
|
||||||
},
|
|
||||||
styleExternalIcon() {
|
|
||||||
return {
|
|
||||||
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
|
|
||||||
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scope lang="scss">
|
||||||
|
.sub-el-icon,
|
||||||
|
.nav-icon {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-right: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: -0.15em;
|
position: relative;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
overflow: hidden;
|
vertical-align: -2px;
|
||||||
}
|
|
||||||
|
|
||||||
.svg-external-icon {
|
|
||||||
background-color: currentColor;
|
|
||||||
mask-size: cover!important;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import * as components from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app) => {
|
||||||
|
for (const key in components) {
|
||||||
|
const componentConfig = components[key]
|
||||||
|
app.component(componentConfig.name, componentConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-color-picker
|
|
||||||
v-model="theme"
|
|
||||||
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
|
|
||||||
class="theme-picker"
|
|
||||||
popper-class="theme-picker-dropdown"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const ORIGINAL_THEME = '#409EFF' // default color
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
chalk: '', // content of theme-chalk css
|
|
||||||
theme: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
defaultTheme() {
|
|
||||||
return this.$store.state.settings.theme
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
defaultTheme: {
|
|
||||||
handler: function(val, oldVal) {
|
|
||||||
this.theme = val
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
},
|
|
||||||
async theme(val) {
|
|
||||||
await this.setTheme(val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if(this.defaultTheme !== ORIGINAL_THEME) {
|
|
||||||
this.setTheme(this.defaultTheme)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async setTheme(val) {
|
|
||||||
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
|
|
||||||
if (typeof val !== 'string') return
|
|
||||||
const themeCluster = this.getThemeCluster(val.replace('#', ''))
|
|
||||||
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
|
|
||||||
|
|
||||||
const getHandler = (variable, id) => {
|
|
||||||
return () => {
|
|
||||||
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
|
|
||||||
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
|
|
||||||
|
|
||||||
let styleTag = document.getElementById(id)
|
|
||||||
if (!styleTag) {
|
|
||||||
styleTag = document.createElement('style')
|
|
||||||
styleTag.setAttribute('id', id)
|
|
||||||
document.head.appendChild(styleTag)
|
|
||||||
}
|
|
||||||
styleTag.innerText = newStyle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.chalk) {
|
|
||||||
const url = `/styles/theme-chalk/index.css`
|
|
||||||
await this.getCSSString(url, 'chalk')
|
|
||||||
}
|
|
||||||
|
|
||||||
const chalkHandler = getHandler('chalk', 'chalk-style')
|
|
||||||
chalkHandler()
|
|
||||||
|
|
||||||
const styles = [].slice.call(document.querySelectorAll('style'))
|
|
||||||
.filter(style => {
|
|
||||||
const text = style.innerText
|
|
||||||
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
|
|
||||||
})
|
|
||||||
styles.forEach(style => {
|
|
||||||
const { innerText } = style
|
|
||||||
if (typeof innerText !== 'string') return
|
|
||||||
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$emit('change', val)
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStyle(style, oldCluster, newCluster) {
|
|
||||||
let newStyle = style
|
|
||||||
oldCluster.forEach((color, index) => {
|
|
||||||
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
|
|
||||||
})
|
|
||||||
return newStyle
|
|
||||||
},
|
|
||||||
|
|
||||||
getCSSString(url, variable) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const xhr = new XMLHttpRequest()
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
||||||
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('GET', url)
|
|
||||||
xhr.send()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
getThemeCluster(theme) {
|
|
||||||
const tintColor = (color, tint) => {
|
|
||||||
let red = parseInt(color.slice(0, 2), 16)
|
|
||||||
let green = parseInt(color.slice(2, 4), 16)
|
|
||||||
let blue = parseInt(color.slice(4, 6), 16)
|
|
||||||
|
|
||||||
if (tint === 0) { // when primary color is in its rgb space
|
|
||||||
return [red, green, blue].join(',')
|
|
||||||
} else {
|
|
||||||
red += Math.round(tint * (255 - red))
|
|
||||||
green += Math.round(tint * (255 - green))
|
|
||||||
blue += Math.round(tint * (255 - blue))
|
|
||||||
|
|
||||||
red = red.toString(16)
|
|
||||||
green = green.toString(16)
|
|
||||||
blue = blue.toString(16)
|
|
||||||
|
|
||||||
return `#${red}${green}${blue}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shadeColor = (color, shade) => {
|
|
||||||
let red = parseInt(color.slice(0, 2), 16)
|
|
||||||
let green = parseInt(color.slice(2, 4), 16)
|
|
||||||
let blue = parseInt(color.slice(4, 6), 16)
|
|
||||||
|
|
||||||
red = Math.round((1 - shade) * red)
|
|
||||||
green = Math.round((1 - shade) * green)
|
|
||||||
blue = Math.round((1 - shade) * blue)
|
|
||||||
|
|
||||||
red = red.toString(16)
|
|
||||||
green = green.toString(16)
|
|
||||||
blue = blue.toString(16)
|
|
||||||
|
|
||||||
return `#${red}${green}${blue}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const clusters = [theme]
|
|
||||||
for (let i = 0; i <= 9; i++) {
|
|
||||||
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
|
|
||||||
}
|
|
||||||
clusters.push(shadeColor(theme, 0.1))
|
|
||||||
return clusters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.theme-message,
|
|
||||||
.theme-picker-dropdown {
|
|
||||||
z-index: 99999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-picker .el-color-picker__trigger {
|
|
||||||
height: 26px !important;
|
|
||||||
width: 26px !important;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-picker-dropdown .el-color-dropdown__link-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading" :style="'height:' + height">
|
<div v-loading="loading" :style="'height:' + height">
|
||||||
<iframe
|
<iframe
|
||||||
:src="src"
|
:src="url"
|
||||||
frameborder="no"
|
frameborder="no"
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
scrolling="auto"
|
scrolling="auto" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
export default {
|
<script setup>
|
||||||
props: {
|
const props = defineProps({
|
||||||
src: {
|
src: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
height: document.documentElement.clientHeight - 94.5 + "px;",
|
|
||||||
loading: true,
|
|
||||||
url: this.src
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.loading = false
|
|
||||||
}, 300)
|
|
||||||
const that = this
|
|
||||||
window.onresize = function temp() {
|
|
||||||
that.height = document.documentElement.clientHeight - 94.5 + "px;"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
|
||||||
|
const loading = ref(true)
|
||||||
|
const url = computed(() => props.src)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
}, 300)
|
||||||
|
window.onresize = function temp() {
|
||||||
|
height.value = document.documentElement.clientHeight - 94.5 + "px;"
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* v-copyText 复制文本内容
|
||||||
|
* Copyright (c) 2022 ruoyi
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
beforeMount(el, { value, arg }) {
|
||||||
|
if (arg === "callback") {
|
||||||
|
el.$copyCallback = value
|
||||||
|
} else {
|
||||||
|
el.$copyValue = value
|
||||||
|
const handler = () => {
|
||||||
|
copyTextToClipboard(el.$copyValue)
|
||||||
|
if (el.$copyCallback) {
|
||||||
|
el.$copyCallback(el.$copyValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el.addEventListener("click", handler)
|
||||||
|
el.$destroyCopy = () => el.removeEventListener("click", handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTextToClipboard(input, { target = document.body } = {}) {
|
||||||
|
const element = document.createElement('textarea')
|
||||||
|
const previouslyFocusedElement = document.activeElement
|
||||||
|
|
||||||
|
element.value = input
|
||||||
|
|
||||||
|
// Prevent keyboard from showing on mobile
|
||||||
|
element.setAttribute('readonly', '')
|
||||||
|
|
||||||
|
element.style.contain = 'strict'
|
||||||
|
element.style.position = 'absolute'
|
||||||
|
element.style.left = '-9999px'
|
||||||
|
element.style.fontSize = '12pt' // Prevent zooming on iOS
|
||||||
|
|
||||||
|
const selection = document.getSelection()
|
||||||
|
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0)
|
||||||
|
|
||||||
|
target.append(element)
|
||||||
|
element.select()
|
||||||
|
|
||||||
|
// Explicit selection workaround for iOS
|
||||||
|
element.selectionStart = 0
|
||||||
|
element.selectionEnd = input.length
|
||||||
|
|
||||||
|
let isSuccess = false
|
||||||
|
try {
|
||||||
|
isSuccess = document.execCommand('copy')
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
element.remove()
|
||||||
|
|
||||||
|
if (originalRange) {
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(originalRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the focus back on the previously focused element, if any
|
||||||
|
if (previouslyFocusedElement) {
|
||||||
|
previouslyFocusedElement.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSuccess
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* v-dialogDrag 弹窗拖拽
|
|
||||||
* Copyright (c) 2019 ruoyi
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
bind(el, binding, vnode, oldVnode) {
|
|
||||||
const value = binding.value
|
|
||||||
if (value == false) return
|
|
||||||
// 获取拖拽内容头部
|
|
||||||
const dialogHeaderEl = el.querySelector('.el-dialog__header')
|
|
||||||
const dragDom = el.querySelector('.el-dialog')
|
|
||||||
dialogHeaderEl.style.cursor = 'move'
|
|
||||||
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null)
|
|
||||||
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
|
|
||||||
dragDom.style.position = 'absolute'
|
|
||||||
dragDom.style.marginTop = 0
|
|
||||||
let width = dragDom.style.width
|
|
||||||
if (width.includes('%')) {
|
|
||||||
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100)
|
|
||||||
} else {
|
|
||||||
width = +width.replace(/\px/g, '')
|
|
||||||
}
|
|
||||||
dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`
|
|
||||||
// 鼠标按下事件
|
|
||||||
dialogHeaderEl.onmousedown = (e) => {
|
|
||||||
// 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
|
|
||||||
const disX = e.clientX - dialogHeaderEl.offsetLeft
|
|
||||||
const disY = e.clientY - dialogHeaderEl.offsetTop
|
|
||||||
|
|
||||||
// 获取到的值带px 正则匹配替换
|
|
||||||
let styL, styT
|
|
||||||
|
|
||||||
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
|
|
||||||
if (sty.left.includes('%')) {
|
|
||||||
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
|
|
||||||
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
|
|
||||||
} else {
|
|
||||||
styL = +sty.left.replace(/\px/g, '')
|
|
||||||
styT = +sty.top.replace(/\px/g, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标拖拽事件
|
|
||||||
document.onmousemove = function (e) {
|
|
||||||
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
|
|
||||||
const l = e.clientX - disX
|
|
||||||
const t = e.clientY - disY
|
|
||||||
|
|
||||||
let finallyL = l + styL
|
|
||||||
let finallyT = t + styT
|
|
||||||
|
|
||||||
// 移动当前元素
|
|
||||||
dragDom.style.left = `${finallyL}px`
|
|
||||||
dragDom.style.top = `${finallyT}px`
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
document.onmouseup = function (e) {
|
|
||||||
document.onmousemove = null
|
|
||||||
document.onmouseup = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* v-dialogDragWidth 可拖动弹窗高度(右下角)
|
|
||||||
* Copyright (c) 2019 ruoyi
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
bind(el) {
|
|
||||||
const dragDom = el.querySelector('.el-dialog')
|
|
||||||
const lineEl = document.createElement('div')
|
|
||||||
lineEl.style = 'width: 6px; background: inherit; height: 10px; position: absolute; right: 0; bottom: 0; margin: auto; z-index: 1; cursor: nwse-resize;'
|
|
||||||
lineEl.addEventListener('mousedown',
|
|
||||||
function(e) {
|
|
||||||
// 鼠标按下,计算当前元素距离可视区的距离
|
|
||||||
const disX = e.clientX - el.offsetLeft
|
|
||||||
const disY = e.clientY - el.offsetTop
|
|
||||||
// 当前宽度 高度
|
|
||||||
const curWidth = dragDom.offsetWidth
|
|
||||||
const curHeight = dragDom.offsetHeight
|
|
||||||
document.onmousemove = function(e) {
|
|
||||||
e.preventDefault() // 移动时禁用默认事件
|
|
||||||
// 通过事件委托,计算移动的距离
|
|
||||||
const xl = e.clientX - disX
|
|
||||||
const yl = e.clientY - disY
|
|
||||||
dragDom.style.width = `${curWidth + xl}px`
|
|
||||||
dragDom.style.height = `${curHeight + yl}px`
|
|
||||||
}
|
|
||||||
document.onmouseup = function(e) {
|
|
||||||
document.onmousemove = null
|
|
||||||
document.onmouseup = null
|
|
||||||
}
|
|
||||||
}, false)
|
|
||||||
dragDom.appendChild(lineEl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* v-dialogDragWidth 可拖动弹窗宽度(右侧边)
|
|
||||||
* Copyright (c) 2019 ruoyi
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
bind(el) {
|
|
||||||
const dragDom = el.querySelector('.el-dialog')
|
|
||||||
const lineEl = document.createElement('div')
|
|
||||||
lineEl.style = 'width: 5px; background: inherit; height: 80%; position: absolute; right: 0; top: 0; bottom: 0; margin: auto; z-index: 1; cursor: w-resize;'
|
|
||||||
lineEl.addEventListener('mousedown',
|
|
||||||
function (e) {
|
|
||||||
// 鼠标按下,计算当前元素距离可视区的距离
|
|
||||||
const disX = e.clientX - el.offsetLeft
|
|
||||||
// 当前宽度
|
|
||||||
const curWidth = dragDom.offsetWidth
|
|
||||||
document.onmousemove = function (e) {
|
|
||||||
e.preventDefault() // 移动时禁用默认事件
|
|
||||||
// 通过事件委托,计算移动的距离
|
|
||||||
const l = e.clientX - disX
|
|
||||||
dragDom.style.width = `${curWidth + l}px`
|
|
||||||
}
|
|
||||||
document.onmouseup = function (e) {
|
|
||||||
document.onmousemove = null
|
|
||||||
document.onmouseup = null
|
|
||||||
}
|
|
||||||
}, false)
|
|
||||||
dragDom.appendChild(lineEl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,9 @@
|
|||||||
import hasRole from './permission/hasRole'
|
import hasRole from './permission/hasRole'
|
||||||
import hasPermi from './permission/hasPermi'
|
import hasPermi from './permission/hasPermi'
|
||||||
import dialogDrag from './dialog/drag'
|
import copyText from './common/copyText'
|
||||||
import dialogDragWidth from './dialog/dragWidth'
|
|
||||||
import dialogDragHeight from './dialog/dragHeight'
|
|
||||||
import clipboard from './module/clipboard'
|
|
||||||
|
|
||||||
const install = function(Vue) {
|
export default function directive(app){
|
||||||
Vue.directive('hasRole', hasRole)
|
app.directive('hasRole', hasRole)
|
||||||
Vue.directive('hasPermi', hasPermi)
|
app.directive('hasPermi', hasPermi)
|
||||||
Vue.directive('clipboard', clipboard)
|
app.directive('copyText', copyText)
|
||||||
Vue.directive('dialogDrag', dialogDrag)
|
}
|
||||||
Vue.directive('dialogDragWidth', dialogDragWidth)
|
|
||||||
Vue.directive('dialogDragHeight', dialogDragHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.Vue) {
|
|
||||||
window['hasRole'] = hasRole
|
|
||||||
window['hasPermi'] = hasPermi
|
|
||||||
Vue.use(install)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default install
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* v-clipboard 文字复制剪贴
|
|
||||||
* Copyright (c) 2021 ruoyi
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Clipboard from 'clipboard'
|
|
||||||
export default {
|
|
||||||
bind(el, binding, vnode) {
|
|
||||||
switch (binding.arg) {
|
|
||||||
case 'success':
|
|
||||||
el._vClipBoard_success = binding.value
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
el._vClipBoard_error = binding.value
|
|
||||||
break
|
|
||||||
default: {
|
|
||||||
const clipboard = new Clipboard(el, {
|
|
||||||
text: () => binding.value,
|
|
||||||
action: () => binding.arg === 'cut' ? 'cut' : 'copy'
|
|
||||||
})
|
|
||||||
clipboard.on('success', e => {
|
|
||||||
const callback = el._vClipBoard_success
|
|
||||||
callback && callback(e)
|
|
||||||
})
|
|
||||||
clipboard.on('error', e => {
|
|
||||||
const callback = el._vClipBoard_error
|
|
||||||
callback && callback(e)
|
|
||||||
})
|
|
||||||
el._vClipBoard = clipboard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update(el, binding) {
|
|
||||||
if (binding.arg === 'success') {
|
|
||||||
el._vClipBoard_success = binding.value
|
|
||||||
} else if (binding.arg === 'error') {
|
|
||||||
el._vClipBoard_error = binding.value
|
|
||||||
} else {
|
|
||||||
el._vClipBoard.text = function () { return binding.value }
|
|
||||||
el._vClipBoard.action = () => binding.arg === 'cut' ? 'cut' : 'copy'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unbind(el, binding) {
|
|
||||||
if (!el._vClipboard) return
|
|
||||||
if (binding.arg === 'success') {
|
|
||||||
delete el._vClipBoard_success
|
|
||||||
} else if (binding.arg === 'error') {
|
|
||||||
delete el._vClipBoard_error
|
|
||||||
} else {
|
|
||||||
el._vClipBoard.destroy()
|
|
||||||
delete el._vClipBoard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,13 @@
|
|||||||
* v-hasPermi 操作权限处理
|
* v-hasPermi 操作权限处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 ruoyi
|
||||||
*/
|
*/
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
import store from '@/store'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inserted(el, binding, vnode) {
|
mounted(el, binding, vnode) {
|
||||||
const { value } = binding
|
const { value } = binding
|
||||||
const all_permission = "*:*:*"
|
const all_permission = "*:*:*"
|
||||||
const permissions = store.getters && store.getters.permissions
|
const permissions = useUserStore().permissions
|
||||||
|
|
||||||
if (value && value instanceof Array && value.length > 0) {
|
if (value && value instanceof Array && value.length > 0) {
|
||||||
const permissionFlag = value
|
const permissionFlag = value
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
* v-hasRole 角色权限处理
|
* v-hasRole 角色权限处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 ruoyi
|
||||||
*/
|
*/
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
import store from '@/store'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inserted(el, binding, vnode) {
|
mounted(el, binding, vnode) {
|
||||||
const { value } = binding
|
const { value } = binding
|
||||||
const super_admin = "admin"
|
const super_admin = "admin"
|
||||||
const roles = store.getters && store.getters.roles
|
const roles = useUserStore().roles
|
||||||
|
|
||||||
if (value && value instanceof Array && value.length > 0) {
|
if (value && value instanceof Array && value.length > 0) {
|
||||||
const roleFlag = value
|
const roleFlag = value
|
||||||
@@ -22,7 +21,7 @@ export default {
|
|||||||
el.parentNode && el.parentNode.removeChild(el)
|
el.parentNode && el.parentNode.removeChild(el)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`请设置角色权限标签值"`)
|
throw new Error(`请设置角色权限标签值`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="app-main">
|
<section class="app-main">
|
||||||
<transition name="fade-transform" mode="out-in">
|
<router-view v-slot="{ Component, route }">
|
||||||
<keep-alive :include="cachedViews">
|
<transition name="fade-transform" mode="out-in">
|
||||||
<router-view v-if="!$route.meta.link" :key="key" />
|
<keep-alive :include="tagsViewStore.cachedViews">
|
||||||
</keep-alive>
|
<component v-if="!route.meta.link" :is="Component" :key="route.path"/>
|
||||||
</transition>
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
<iframe-toggle />
|
<iframe-toggle />
|
||||||
<copyright />
|
<copyright />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import copyright from "./Copyright/index"
|
import copyright from "./Copyright/index"
|
||||||
import iframeToggle from "./IframeToggle/index"
|
import iframeToggle from "./IframeToggle/index"
|
||||||
|
import useTagsViewStore from '@/store/modules/tagsView'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
name: 'AppMain',
|
const tagsViewStore = useTagsViewStore()
|
||||||
components: { iframeToggle, copyright },
|
|
||||||
computed: {
|
onMounted(() => {
|
||||||
cachedViews() {
|
addIframe()
|
||||||
return this.$store.state.tagsView.cachedViews
|
})
|
||||||
},
|
|
||||||
key() {
|
watchEffect(() => {
|
||||||
return this.$route.path
|
addIframe()
|
||||||
}
|
})
|
||||||
},
|
|
||||||
watch: {
|
function addIframe() {
|
||||||
$route() {
|
if (route.meta.link) {
|
||||||
this.addIframe()
|
useTagsViewStore().addIframeView(route)
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.addIframe()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addIframe() {
|
|
||||||
const { name } = this.$route
|
|
||||||
if (name && this.$route.meta.link) {
|
|
||||||
this.$store.dispatch('tagsView/addIframeView', this.$route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -51,14 +42,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:fullscreen,
|
|
||||||
&:-webkit-full-screen,
|
|
||||||
&:-moz-full-screen,
|
|
||||||
&:-ms-fullscreen {
|
|
||||||
background: #fff;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-header + .app-main {
|
.fixed-header + .app-main {
|
||||||
@@ -137,4 +120,4 @@ export default {
|
|||||||
background-color: #c0c0c0;
|
background-color: #c0c0c0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
computed: {
|
|
||||||
visible() {
|
const settingsStore = useSettingsStore()
|
||||||
return this.$store.state.settings.footerVisible
|
|
||||||
},
|
const visible = computed(() => settingsStore.footerVisible)
|
||||||
content() {
|
const content = computed(() => settingsStore.footerContent)
|
||||||
return this.$store.state.settings.footerContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer title="公告详情" :visible.sync="visible" direction="rtl" size="50%" append-to-body :before-close="handleClose" custom-class="notice-detail-drawer">
|
<el-drawer v-model="visible" title="公告详情" direction="rtl" size="50%" append-to-body :before-close="handleClose" class="notice-detail-drawer">
|
||||||
<div v-loading="loading" class="notice-detail-drawer__body">
|
<div v-loading="loading" class="notice-detail-drawer__body">
|
||||||
<div v-if="!detail" class="notice-empty">
|
<div v-if="!detail" class="notice-empty">
|
||||||
<i class="el-icon-document"></i>
|
<el-icon><Document /></el-icon>
|
||||||
<span>暂无数据</span>
|
<span>暂无数据</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="notice-page">
|
<div v-else class="notice-page">
|
||||||
<div class="notice-type-wrap">
|
<div class="notice-type-wrap">
|
||||||
<span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
|
<span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
|
||||||
<i class="el-icon-bell"></i> 通知
|
<el-icon><Bell /></el-icon> 通知
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
|
<span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
|
||||||
<i class="el-icon-message"></i> 公告
|
<el-icon><Message /></el-icon> 公告
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="notice-type-tag type-notify">
|
<span v-else class="notice-type-tag type-notify">
|
||||||
<i class="el-icon-document"></i> 消息
|
<el-icon><Document /></el-icon> 消息
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
<div class="notice-meta">
|
<div class="notice-meta">
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<i class="el-icon-user"></i>
|
<el-icon><User /></el-icon>
|
||||||
<span>{{ detail.createBy || '—' }}</span>
|
<span>{{ detail.createBy || '—' }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<i class="el-icon-time"></i>
|
<el-icon><Clock /></el-icon>
|
||||||
<span>{{ detail.createTime || '—' }}</span>
|
<span>{{ detail.createTime || '—' }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="notice-body">
|
<div class="notice-body">
|
||||||
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
|
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
|
||||||
<div v-else class="notice-empty notice-empty--inner">
|
<div v-else class="notice-empty notice-empty--inner">
|
||||||
<i class="el-icon-document"></i> 暂无内容
|
<el-icon><Document /></el-icon> 暂无内容
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,66 +52,63 @@
|
|||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { getNotice } from '@/api/system/notice'
|
import { getNotice } from '@/api/system/notice'
|
||||||
|
|
||||||
export default {
|
const visible = ref(false)
|
||||||
name: 'NoticeDetailView',
|
const loading = ref(false)
|
||||||
data() {
|
const detail = ref(null)
|
||||||
return {
|
|
||||||
visible: false,
|
const isStatusNormal = computed(() => {
|
||||||
loading: false,
|
const status = detail.value && detail.value.status
|
||||||
detail: null
|
return status === '0' || status === 0
|
||||||
}
|
})
|
||||||
},
|
|
||||||
computed: {
|
const hasContent = computed(() => {
|
||||||
isStatusNormal() {
|
const content = detail.value && detail.value.noticeContent
|
||||||
const s = this.detail && this.detail.status
|
return content != null && String(content).trim() !== ''
|
||||||
return s === '0' || s === 0
|
})
|
||||||
},
|
|
||||||
hasContent() {
|
function open(payload) {
|
||||||
const c = this.detail && this.detail.noticeContent
|
let id = null
|
||||||
return c != null && String(c).trim() !== ''
|
let preset = null
|
||||||
}
|
if (payload != null && typeof payload === 'object') {
|
||||||
},
|
id = payload.noticeId
|
||||||
methods: {
|
if (payload.noticeContent != null) {
|
||||||
open(payload) {
|
preset = payload
|
||||||
let id = null
|
|
||||||
let preset = null
|
|
||||||
if (payload != null && typeof payload === 'object') {
|
|
||||||
id = payload.noticeId
|
|
||||||
if (payload.noticeContent != null) {
|
|
||||||
preset = payload
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = payload
|
|
||||||
}
|
|
||||||
this.visible = true
|
|
||||||
if (preset) {
|
|
||||||
this.detail = preset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (id == null || id === '') {
|
|
||||||
this.detail = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.loading = true
|
|
||||||
this.detail = null
|
|
||||||
getNotice(id).then(res => {
|
|
||||||
this.detail = res.data
|
|
||||||
}).catch(() => {
|
|
||||||
this.detail = null
|
|
||||||
}).finally(() => {
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleClose() {
|
|
||||||
this.visible = false
|
|
||||||
this.detail = null
|
|
||||||
this.loading = false
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
id = payload
|
||||||
}
|
}
|
||||||
|
visible.value = true
|
||||||
|
if (preset) {
|
||||||
|
detail.value = preset
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (id == null || id === '') {
|
||||||
|
detail.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
detail.value = null
|
||||||
|
getNotice(id).then(res => {
|
||||||
|
detail.value = res.data
|
||||||
|
}).catch(() => {
|
||||||
|
detail.value = null
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
detail.value = null
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -186,7 +183,7 @@ export default {
|
|||||||
color: #718096;
|
color: #718096;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-item i {
|
.meta-item .el-icon {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #a0aec0;
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
@@ -244,56 +241,56 @@ export default {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep p {
|
.notice-content :deep(p) {
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep h1,
|
.notice-content :deep(h1),
|
||||||
.notice-content ::v-deep h2,
|
.notice-content :deep(h2),
|
||||||
.notice-content ::v-deep h3 {
|
.notice-content :deep(h3) {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a202c;
|
color: #1a202c;
|
||||||
margin: 1.4em 0 0.6em;
|
margin: 1.4em 0 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep h1 {
|
.notice-content :deep(h1) {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep h2 {
|
.notice-content :deep(h2) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep h3 {
|
.notice-content :deep(h3) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep a {
|
.notice-content :deep(a) {
|
||||||
color: #3182ce;
|
color: #3182ce;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep a:hover {
|
.notice-content :deep(a:hover) {
|
||||||
color: #2b6cb0;
|
color: #2b6cb0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep img {
|
.notice-content :deep(img) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep ul,
|
.notice-content :deep(ul),
|
||||||
.notice-content ::v-deep ol {
|
.notice-content :deep(ol) {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep li {
|
.notice-content :deep(li) {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep blockquote {
|
.notice-content :deep(blockquote) {
|
||||||
border-left: 3px solid #cbd5e0;
|
border-left: 3px solid #cbd5e0;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
@@ -301,20 +298,20 @@ export default {
|
|||||||
background: #f7fafc;
|
background: #f7fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep table {
|
.notice-content :deep(table) {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep table th,
|
.notice-content :deep(table th),
|
||||||
.notice-content ::v-deep table td {
|
.notice-content :deep(table td) {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-content ::v-deep table th {
|
.notice-content :deep(table th) {
|
||||||
background: #f7fafc;
|
background: #f7fafc;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -326,9 +323,9 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-empty i {
|
.notice-empty .el-icon {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
display: block;
|
display: inline-flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,27 +333,27 @@ export default {
|
|||||||
padding: 32px 0;
|
padding: 32px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-empty--inner i {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .notice-detail-drawer {
|
|
||||||
.el-drawer__header {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
background: #f5f6f8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-detail-drawer__body {
|
.notice-detail-drawer__body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px 16px 22px;
|
padding: 10px 16px 22px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notice-detail-drawer {
|
||||||
|
.el-drawer__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,101 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-popover ref="noticePopover" placement="bottom-end" width="320" trigger="manual" :value="noticeVisible" popper-class="notice-popover">
|
<el-popover ref="noticePopover" placement="bottom-end" :width="320" trigger="manual" v-model:visible="noticeVisible" popper-class="notice-popover">
|
||||||
|
<!-- 弹出内容 -->
|
||||||
<div class="notice-header">
|
<div class="notice-header">
|
||||||
<span class="notice-title">通知公告</span>
|
<span class="notice-title">通知公告</span>
|
||||||
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
|
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="noticeLoading" class="notice-loading"><i class="el-icon-loading"></i> 加载中...</div>
|
<div v-if="noticeLoading" class="notice-loading">
|
||||||
<div v-else-if="noticeList.length === 0" class="notice-empty"><i class="el-icon-inbox"></i><br>暂无公告</div>
|
<el-icon class="is-loading"><Loading /></el-icon> 加载中...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="noticeList.length === 0" class="notice-empty">
|
||||||
|
<el-icon style="font-size:24px;display:block;margin-bottom:6px;"><Postcard /></el-icon>
|
||||||
|
暂无公告
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
|
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
|
||||||
<el-tag size="mini" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
|
<el-tag size="small" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
|
||||||
{{ item.noticeType === '1' ? '通知' : '公告' }}
|
{{ item.noticeType === '1' ? '通知' : '公告' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="notice-item-title">{{ item.noticeTitle }}</span>
|
<span class="notice-item-title">{{ item.noticeTitle }}</span>
|
||||||
<span class="notice-item-date">{{ item.createTime }}</span>
|
<span class="notice-item-date">{{ item.createTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 触发器 -->
|
||||||
|
<template #reference>
|
||||||
|
<div class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
|
||||||
|
<svg-icon icon-class="bell" />
|
||||||
|
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
<div v-popover:noticePopover class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
|
<!-- 预览弹窗 -->
|
||||||
<svg-icon icon-class="bell" />
|
|
||||||
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<notice-detail-view ref="noticeViewRef" />
|
<notice-detail-view ref="noticeViewRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import NoticeDetailView from './DetailView'
|
import NoticeDetailView from './DetailView'
|
||||||
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
|
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
|
||||||
|
|
||||||
export default {
|
const noticePopover = ref(null)
|
||||||
name: 'HeaderNotice',
|
const noticeList = ref([])
|
||||||
components: { NoticeDetailView },
|
const unreadCount = ref(0)
|
||||||
data() {
|
const noticeLoading = ref(false)
|
||||||
return {
|
const noticeVisible = ref(false)
|
||||||
noticeList: [], // 通知列表
|
const noticeLeaveTimer = ref(null)
|
||||||
unreadCount: 0, // 未读数量
|
const { proxy } = getCurrentInstance()
|
||||||
noticeLoading: false, // 加载状态
|
|
||||||
noticeVisible: false, // 弹出层显示状态
|
// 加载顶部公告列表
|
||||||
noticeLeaveTimer: null // 鼠标离开计时器
|
function loadNoticeTop() {
|
||||||
}
|
noticeLoading.value = true
|
||||||
},
|
listNoticeTop().then(res => {
|
||||||
mounted() {
|
noticeList.value = res.data || []
|
||||||
this.loadNoticeTop()
|
unreadCount.value = res.unreadCount !== undefined ? res.unreadCount : noticeList.value.filter(n => !n.isRead).length
|
||||||
},
|
}).finally(() => {
|
||||||
methods: {
|
noticeLoading.value = false
|
||||||
// 鼠标移入铃铛区域
|
})
|
||||||
onNoticeEnter() {
|
}
|
||||||
clearTimeout(this.noticeLeaveTimer)
|
|
||||||
this.noticeVisible = true
|
onMounted(() => loadNoticeTop())
|
||||||
this.$nextTick(() => {
|
|
||||||
const popper = this.$refs.noticePopover.$refs.popper
|
// 鼠标移入铃铛区域
|
||||||
if (popper && !popper._noticeBound) {
|
function onNoticeEnter() {
|
||||||
popper._noticeBound = true
|
clearTimeout(noticeLeaveTimer.value)
|
||||||
popper.addEventListener('mouseenter', () => clearTimeout(this.noticeLeaveTimer))
|
noticeVisible.value = true
|
||||||
popper.addEventListener('mouseleave', () => {
|
nextTick(() => {
|
||||||
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 100)
|
const popper = noticePopover.value?.popperRef?.contentRef
|
||||||
})
|
if (popper && !popper._noticeBound) {
|
||||||
}
|
popper._noticeBound = true
|
||||||
|
popper.addEventListener('mouseenter', () => clearTimeout(noticeLeaveTimer.value))
|
||||||
|
popper.addEventListener('mouseleave', () => {
|
||||||
|
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 100)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
// 鼠标离开铃铛区域
|
|
||||||
onNoticeLeave() {
|
|
||||||
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 150)
|
|
||||||
},
|
|
||||||
// 加载顶部公告列表
|
|
||||||
loadNoticeTop() {
|
|
||||||
this.noticeLoading = true
|
|
||||||
listNoticeTop().then(res => {
|
|
||||||
this.noticeList = res.data || []
|
|
||||||
this.unreadCount = res.unreadCount !== undefined ? res.unreadCount : this.noticeList.filter(n => !n.isRead).length
|
|
||||||
}).finally(() => {
|
|
||||||
this.noticeLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 预览公告详情
|
|
||||||
previewNotice(item) {
|
|
||||||
if (!item.isRead) {
|
|
||||||
markNoticeRead(item.noticeId).catch(() => {})
|
|
||||||
item.isRead = true
|
|
||||||
const idx = this.noticeList.indexOf(item)
|
|
||||||
if (idx !== -1) this.$set(this.noticeList, idx, { ...item, isRead: true })
|
|
||||||
this.unreadCount = Math.max(0, this.unreadCount - 1)
|
|
||||||
}
|
|
||||||
this.$refs.noticeViewRef.open(item.noticeId)
|
|
||||||
},
|
|
||||||
// 全部已读
|
|
||||||
markAllRead() {
|
|
||||||
const ids = this.noticeList.map(n => n.noticeId).join(',')
|
|
||||||
if (!ids) return
|
|
||||||
markNoticeReadAll(ids).catch(() => {})
|
|
||||||
this.noticeList = this.noticeList.map(n => ({ ...n, isRead: true }))
|
|
||||||
this.unreadCount = 0
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标离开铃铛区域
|
||||||
|
function onNoticeLeave() {
|
||||||
|
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览公告详情
|
||||||
|
function previewNotice(item) {
|
||||||
|
if (!item.isRead) {
|
||||||
|
markNoticeRead(item.noticeId).catch(() => {})
|
||||||
|
const idx = noticeList.value.indexOf(item)
|
||||||
|
if (idx !== -1) noticeList.value[idx] = { ...item, isRead: true }
|
||||||
|
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||||
}
|
}
|
||||||
|
proxy.$refs["noticeViewRef"].open(item.noticeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部已读
|
||||||
|
function markAllRead() {
|
||||||
|
const ids = noticeList.value.map(n => n.noticeId).join(',')
|
||||||
|
if (!ids) return
|
||||||
|
markNoticeReadAll(ids).catch(() => {})
|
||||||
|
noticeList.value = noticeList.value.map(n => ({ ...n, isRead: true }))
|
||||||
|
unreadCount.value = 0
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -121,9 +126,7 @@ export default {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.notice-popover {
|
.notice-popover { padding: 0 !important; }
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.notice-popover .notice-header {
|
.notice-popover .notice-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -137,7 +140,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.notice-popover .notice-mark-all {
|
.notice-popover .notice-mark-all {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #409EFF;
|
color: var(--el-color-primary);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition-group name="fade-transform" mode="out-in">
|
<inner-link
|
||||||
<inner-link
|
v-for="(item, index) in tagsViewStore.iframeViews"
|
||||||
v-for="(item, index) in iframeViews"
|
:key="item.path"
|
||||||
:key="item.path"
|
:iframeId="'iframe' + index"
|
||||||
:iframeId="'iframe' + index"
|
v-show="route.path === item.path"
|
||||||
v-show="$route.path === item.path"
|
:src="iframeUrl(item.meta.link, item.query)"
|
||||||
:src="iframeUrl(item.meta.link, item.query)"
|
></inner-link>
|
||||||
></inner-link>
|
|
||||||
</transition-group>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import InnerLink from "../InnerLink/index"
|
import InnerLink from "../InnerLink/index"
|
||||||
|
import useTagsViewStore from "@/store/modules/tagsView"
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
components: { InnerLink },
|
const tagsViewStore = useTagsViewStore()
|
||||||
computed: {
|
|
||||||
iframeViews() {
|
function iframeUrl(url, query) {
|
||||||
return this.$store.state.tagsView.iframeViews
|
if (Object.keys(query).length > 0) {
|
||||||
}
|
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&")
|
||||||
},
|
return url + "?" + params
|
||||||
methods: {
|
|
||||||
iframeUrl(url, query) {
|
|
||||||
if (Object.keys(query).length > 0) {
|
|
||||||
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&")
|
|
||||||
return url + "?" + params
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,44 +4,32 @@
|
|||||||
:id="iframeId"
|
:id="iframeId"
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
:src="src"
|
:src="src"
|
||||||
|
ref="iframeRef"
|
||||||
frameborder="no"
|
frameborder="no"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
src: {
|
||||||
src: {
|
type: String,
|
||||||
type: String,
|
default: "/"
|
||||||
default: "/"
|
|
||||||
},
|
|
||||||
iframeId: {
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
iframeId: {
|
||||||
return {
|
type: String
|
||||||
loading: false,
|
}
|
||||||
height: document.documentElement.clientHeight - 94.5 + "px;"
|
})
|
||||||
}
|
|
||||||
},
|
const loading = ref(true)
|
||||||
mounted() {
|
const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
|
||||||
var _this = this
|
const iframeRef = ref(null)
|
||||||
const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/")
|
|
||||||
const iframe = document.querySelector(iframeId)
|
onMounted(() => {
|
||||||
// iframe页面loading控制
|
if (iframeRef.value) {
|
||||||
if (iframe.attachEvent) {
|
iframeRef.value.onload = () => {
|
||||||
this.loading = true
|
loading.value = false
|
||||||
iframe.attachEvent("onload", function () {
|
|
||||||
_this.loading = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.loading = true
|
|
||||||
iframe.onload = function () {
|
|
||||||
_this.loading = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navbar" :class="'nav' + navType">
|
<div class="navbar" :class="'nav' + settingsStore.navType">
|
||||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||||
|
<breadcrumb v-if="settingsStore.navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
|
||||||
<breadcrumb v-if="navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
|
<top-nav v-if="settingsStore.navType == 2" id="topmenu-container" class="topmenu-container" />
|
||||||
<top-nav v-if="navType == 2" id="topmenu-container" class="topmenu-container" />
|
<template v-if="settingsStore.navType == 3">
|
||||||
<template v-if="navType == 3">
|
<logo v-show="settingsStore.sidebarLogo" :collapse="false"></logo>
|
||||||
<logo v-show="showLogo" :collapse="false"></logo>
|
|
||||||
<top-bar id="topbar-container" class="topbar-container" />
|
<top-bar id="topbar-container" class="topbar-container" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="right-menu">
|
<div class="right-menu">
|
||||||
<template v-if="device!=='mobile'">
|
<template v-if="appStore.device !== 'mobile'">
|
||||||
<search id="header-search" class="right-menu-item" />
|
<header-search id="header-search" class="right-menu-item" />
|
||||||
|
|
||||||
<el-tooltip content="源码地址" effect="dark" placement="bottom">
|
<el-tooltip content="源码地址" effect="dark" placement="bottom">
|
||||||
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
|
|
||||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||||
|
|
||||||
|
<el-tooltip content="主题模式" effect="dark" placement="bottom">
|
||||||
|
<div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
|
||||||
|
<svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
|
||||||
|
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -29,35 +36,36 @@
|
|||||||
<el-tooltip content="消息通知" effect="dark" placement="bottom">
|
<el-tooltip content="消息通知" effect="dark" placement="bottom">
|
||||||
<header-notice id="header-notice" class="right-menu-item hover-effect" />
|
<header-notice id="header-notice" class="right-menu-item hover-effect" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
|
<el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
<img :src="avatar" class="user-avatar">
|
<img :src="userStore.avatar" class="user-avatar" />
|
||||||
<span class="user-nickname"> {{ nickName }} </span>
|
<span class="user-nickname"> {{ userStore.nickName }} </span>
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<template #dropdown>
|
||||||
<router-link to="/user/profile">
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
<router-link to="/user/profile">
|
||||||
</router-link>
|
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="setLayout" v-if="setting">
|
</router-link>
|
||||||
<span>布局设置</span>
|
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
|
||||||
</el-dropdown-item>
|
<span>布局设置</span>
|
||||||
<el-dropdown-item @click.native="lockScreen">
|
</el-dropdown-item>
|
||||||
<span>锁定屏幕</span>
|
<el-dropdown-item command="lockScreen">
|
||||||
</el-dropdown-item>
|
<span>锁定屏幕</span>
|
||||||
<el-dropdown-item divided @click.native="logout">
|
</el-dropdown-item>
|
||||||
<span>退出登录</span>
|
<el-dropdown-item divided command="logout">
|
||||||
</el-dropdown-item>
|
<span>退出登录</span>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import Breadcrumb from '@/components/Breadcrumb'
|
import Breadcrumb from '@/components/Breadcrumb'
|
||||||
import TopNav from './TopNav'
|
import TopNav from './TopNav'
|
||||||
import TopBar from './TopBar'
|
import TopBar from './TopBar'
|
||||||
@@ -65,77 +73,107 @@ import Logo from './Sidebar/Logo'
|
|||||||
import Hamburger from '@/components/Hamburger'
|
import Hamburger from '@/components/Hamburger'
|
||||||
import Screenfull from '@/components/Screenfull'
|
import Screenfull from '@/components/Screenfull'
|
||||||
import SizeSelect from '@/components/SizeSelect'
|
import SizeSelect from '@/components/SizeSelect'
|
||||||
import Search from '@/components/HeaderSearch'
|
import HeaderSearch from '@/components/HeaderSearch'
|
||||||
import RuoYiGit from '@/components/RuoYi/Git'
|
import RuoYiGit from '@/components/RuoYi/Git'
|
||||||
import RuoYiDoc from '@/components/RuoYi/Doc'
|
import RuoYiDoc from '@/components/RuoYi/Doc'
|
||||||
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
import useLockStore from '@/store/modules/lock'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
import HeaderNotice from './HeaderNotice'
|
import HeaderNotice from './HeaderNotice'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
components: {
|
const router = useRouter()
|
||||||
Breadcrumb,
|
const appStore = useAppStore()
|
||||||
Logo,
|
const userStore = useUserStore()
|
||||||
TopNav,
|
const lockStore = useLockStore()
|
||||||
TopBar,
|
const settingsStore = useSettingsStore()
|
||||||
Hamburger,
|
|
||||||
Screenfull,
|
function toggleSideBar() {
|
||||||
SizeSelect,
|
appStore.toggleSideBar()
|
||||||
Search,
|
}
|
||||||
RuoYiGit,
|
|
||||||
RuoYiDoc,
|
function handleCommand(command) {
|
||||||
HeaderNotice
|
switch (command) {
|
||||||
},
|
case "setLayout":
|
||||||
computed: {
|
setLayout()
|
||||||
...mapGetters([
|
break
|
||||||
'sidebar',
|
case "lockScreen":
|
||||||
'avatar',
|
lockScreen()
|
||||||
'device',
|
break
|
||||||
'nickName'
|
case "logout":
|
||||||
]),
|
logout()
|
||||||
setting: {
|
break
|
||||||
get() {
|
default:
|
||||||
return this.$store.state.settings.showSettings
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
userStore.logOut().then(() => {
|
||||||
|
location.href = '/index'
|
||||||
|
})
|
||||||
|
}).catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
const emits = defineEmits(['setLayout'])
|
||||||
|
function setLayout() {
|
||||||
|
emits('setLayout')
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockScreen() {
|
||||||
|
const currentPath = route.fullPath
|
||||||
|
lockStore.lockScreen(currentPath)
|
||||||
|
router.push('/lock')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleTheme(event) {
|
||||||
|
const x = event?.clientX || window.innerWidth / 2
|
||||||
|
const y = event?.clientY || window.innerHeight / 2
|
||||||
|
const wasDark = settingsStore.isDark
|
||||||
|
|
||||||
|
const isReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
|
const isSupported = document.startViewTransition && !isReducedMotion
|
||||||
|
|
||||||
|
if (!isSupported) {
|
||||||
|
settingsStore.toggleTheme()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transition = document.startViewTransition(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
settingsStore.toggleTheme()
|
||||||
|
await nextTick()
|
||||||
|
})
|
||||||
|
await transition.ready
|
||||||
|
|
||||||
|
const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y))
|
||||||
|
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
||||||
|
document.documentElement.animate(
|
||||||
|
{
|
||||||
|
clipPath: !wasDark ? [...clipPath].reverse() : clipPath
|
||||||
|
}, {
|
||||||
|
duration: 650,
|
||||||
|
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
fill: "forwards",
|
||||||
|
pseudoElement: !wasDark ? "::view-transition-old(root)" : "::view-transition-new(root)"
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
navType: {
|
await transition.finished
|
||||||
get() {
|
} catch (error) {
|
||||||
return this.$store.state.settings.navType
|
console.warn("View transition failed, falling back to immediate toggle:", error)
|
||||||
}
|
settingsStore.toggleTheme()
|
||||||
},
|
|
||||||
showLogo: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.sidebarLogo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleSideBar() {
|
|
||||||
this.$store.dispatch('app/toggleSideBar')
|
|
||||||
},
|
|
||||||
setLayout(event) {
|
|
||||||
this.$emit('setLayout')
|
|
||||||
},
|
|
||||||
lockScreen() {
|
|
||||||
const currentPath = this.$route.fullPath
|
|
||||||
this.$store.dispatch('lock/lockScreen', currentPath).then(() => {
|
|
||||||
this.$router.push('/lock')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
logout() {
|
|
||||||
this.$confirm('确定注销并退出系统吗?', '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(() => {
|
|
||||||
this.$store.dispatch('LogOut').then(() => {
|
|
||||||
location.href = '/index'
|
|
||||||
})
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
.navbar.nav3 {
|
.navbar.nav3 {
|
||||||
.hamburger-container {
|
.hamburger-container {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -146,8 +184,8 @@ export default {
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: var(--navbar-bg);
|
||||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// padding: 0 8px;
|
// padding: 0 8px;
|
||||||
@@ -157,15 +195,15 @@ export default {
|
|||||||
line-height: 46px;
|
line-height: 46px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .3s;
|
transition: background 0.3s;
|
||||||
-webkit-tap-highlight-color:transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, .025)
|
background: rgba(0, 0, 0, 0.025);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,10 +246,23 @@ export default {
|
|||||||
|
|
||||||
&.hover-effect {
|
&.hover-effect {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .3s;
|
transition: background 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, .025)
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-switch-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,18 +280,19 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-nickname{
|
.user-nickname{
|
||||||
position: relative;
|
position: relative;
|
||||||
|
left: 0px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left: 2px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon-caret-bottom {
|
i {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -20px;
|
right: -20px;
|
||||||
|
|||||||
@@ -1,367 +1,281 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
|
<el-drawer v-model="showSettings" :withHeader="false" :lock-scroll="false" direction="rtl" size="300px">
|
||||||
<div class="drawer-container">
|
<div class="setting-drawer-title">
|
||||||
<div>
|
<h3 class="drawer-title">菜单导航设置</h3>
|
||||||
<div class="setting-drawer-content">
|
</div>
|
||||||
<div class="setting-drawer-title">
|
<div class="nav-wrap">
|
||||||
<h3 class="drawer-title">菜单导航设置</h3>
|
<el-tooltip content="左侧菜单" placement="bottom">
|
||||||
</div>
|
<div class="item left" @click="handleNavType(1)" :class="{ activeItem: navType == 1 }">
|
||||||
<div class="nav-wrap">
|
<b></b><b></b>
|
||||||
<el-tooltip content="左侧菜单" placement="bottom">
|
|
||||||
<div class="item left" @click="handleNavType(1)" :style="{'--theme': theme}" :class="{ activeItem: navType == 1 }">
|
|
||||||
<b></b><b></b>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<el-tooltip content="混合菜单" placement="bottom">
|
|
||||||
<div class="item mix" @click="handleNavType(2)" :style="{'--theme': theme}" :class="{ activeItem: navType == 2 }">
|
|
||||||
<b></b><b></b>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip content="顶部菜单" placement="bottom">
|
|
||||||
<div class="item top" @click="handleNavType(3)" :style="{'--theme': theme}" :class="{ activeItem: navType == 3 }">
|
|
||||||
<b></b><b></b>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="setting-drawer-title">
|
|
||||||
<h3 class="drawer-title">主题风格设置</h3>
|
|
||||||
</div>
|
|
||||||
<div class="setting-drawer-block-checbox">
|
|
||||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
|
||||||
<img src="@/assets/images/dark.svg" alt="dark">
|
|
||||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
|
|
||||||
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
|
||||||
<img src="@/assets/images/light.svg" alt="light">
|
|
||||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
|
|
||||||
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>主题颜色</span>
|
|
||||||
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
<el-divider/>
|
<el-tooltip content="混合菜单" placement="bottom">
|
||||||
|
<div class="item mix" @click="handleNavType(2)" :class="{ activeItem: navType == 2 }">
|
||||||
<h3 class="drawer-title">系统布局配置</h3>
|
<b></b><b></b>
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>开启页签</span>
|
|
||||||
<el-switch v-model="tagsView" class="drawer-switch" />
|
|
||||||
</div>
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
<div class="drawer-item">
|
<el-tooltip content="顶部菜单" placement="bottom">
|
||||||
<span>持久化标签页</span>
|
<div class="item top" @click="handleNavType(3)" :class="{ activeItem: navType == 3 }">
|
||||||
<el-switch v-model="tagsViewPersist" :disabled="!tagsView" class="drawer-switch" />
|
<b></b><b></b>
|
||||||
</div>
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
<div class="drawer-item">
|
</div>
|
||||||
<span>显示页签图标</span>
|
<div class="setting-drawer-title">
|
||||||
<el-switch v-model="tagsIcon" :disabled="!tagsView" class="drawer-switch" />
|
<h3 class="drawer-title">主题风格设置</h3>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer-block-checbox">
|
||||||
|
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
||||||
|
<img src="@/assets/images/dark.svg" alt="dark" />
|
||||||
|
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||||
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
||||||
|
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="drawer-item">
|
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
||||||
<span>标签页样式</span>
|
<img src="@/assets/images/light.svg" alt="light" />
|
||||||
<el-radio-group v-model="tagsViewStyle" :disabled="!tagsView" size="mini" class="drawer-switch">
|
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||||
<el-radio-button label="card">卡片</el-radio-button>
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
<el-radio-button label="chrome">谷歌</el-radio-button>
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
||||||
</el-radio-group>
|
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>固定 Header</span>
|
|
||||||
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>显示 Logo</span>
|
|
||||||
<el-switch v-model="sidebarLogo" class="drawer-switch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>动态标题</span>
|
|
||||||
<el-switch v-model="dynamicTitle" class="drawer-switch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>底部版权</span>
|
|
||||||
<el-switch v-model="footerVisible" class="drawer-switch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider/>
|
|
||||||
|
|
||||||
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
|
|
||||||
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">重置配置</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>主题颜色</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<h3 class="drawer-title">系统布局配置</h3>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>开启页签</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>持久化标签页</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.tagsViewPersist" :disabled="!settingsStore.tagsView" @change="tagsViewPersistChange" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>显示页签图标</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>标签页样式</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-radio-group v-model="settingsStore.tagsViewStyle" :disabled="!settingsStore.tagsView" size="small">
|
||||||
|
<el-radio-button label="card">卡片</el-radio-button>
|
||||||
|
<el-radio-button label="chrome">谷歌</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>固定 Header</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>显示 Logo</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>动态标题</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.dynamicTitle" @change="dynamicTitleChange" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>底部版权</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.footerVisible" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||||
|
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ThemePicker from '@/components/ThemePicker'
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
import { handleThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
export default {
|
const { proxy } = getCurrentInstance()
|
||||||
components: { ThemePicker },
|
const appStore = useAppStore()
|
||||||
expose: ['openSetting'],
|
const settingsStore = useSettingsStore()
|
||||||
data() {
|
const permissionStore = usePermissionStore()
|
||||||
return {
|
const showSettings = ref(false)
|
||||||
theme: this.$store.state.settings.theme,
|
const navType = ref(settingsStore.navType)
|
||||||
sideTheme: this.$store.state.settings.sideTheme,
|
const theme = ref(settingsStore.theme)
|
||||||
navType: this.$store.state.settings.navType,
|
const sideTheme = ref(settingsStore.sideTheme)
|
||||||
showSettings: false
|
const tagsViewPersist = ref(settingsStore.tagsViewPersist)
|
||||||
}
|
const storeSettings = computed(() => settingsStore)
|
||||||
},
|
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"])
|
||||||
computed: {
|
|
||||||
fixedHeader: {
|
/** 是否需要dynamicTitle */
|
||||||
get() {
|
function dynamicTitleChange() {
|
||||||
return this.$store.state.settings.fixedHeader
|
useSettingsStore().setTitle(useSettingsStore().title)
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'fixedHeader',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tagsViewPersist: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.tagsViewPersist
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'tagsViewPersist',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tagsView: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.tagsView
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'tagsView',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tagsIcon: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.tagsIcon
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'tagsIcon',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tagsViewStyle: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.tagsViewStyle
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'tagsViewStyle',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sidebarLogo: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.sidebarLogo
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'sidebarLogo',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dynamicTitle: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.dynamicTitle
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'dynamicTitle',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.$store.dispatch('settings/setTitle', this.$store.state.settings.title)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
footerVisible: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.settings.footerVisible
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'footerVisible',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
navType: {
|
|
||||||
handler(val) {
|
|
||||||
if (val == 1) {
|
|
||||||
this.$store.dispatch("app/toggleSideBarHide", false)
|
|
||||||
}
|
|
||||||
if (val == 2) {
|
|
||||||
}
|
|
||||||
if (val == 3) {
|
|
||||||
this.$store.dispatch("app/toggleSideBarHide", true)
|
|
||||||
}
|
|
||||||
if ([1, 3].includes(val)) {
|
|
||||||
this.$store.commit("SET_SIDEBAR_ROUTERS",this.$store.state.permission.defaultRoutes)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
themeChange(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'theme',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.theme = val
|
|
||||||
},
|
|
||||||
handleTheme(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'sideTheme',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.sideTheme = val
|
|
||||||
},
|
|
||||||
handleNavType(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'navType',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.navType = val
|
|
||||||
},
|
|
||||||
openSetting() {
|
|
||||||
this.showSettings = true
|
|
||||||
},
|
|
||||||
closeSetting(){
|
|
||||||
this.showSettings = false
|
|
||||||
},
|
|
||||||
saveSetting() {
|
|
||||||
this.$modal.loading("正在保存到本地,请稍候...")
|
|
||||||
if (!this.tagsViewPersist) {
|
|
||||||
this.$cache.local.remove('tags-view-visited')
|
|
||||||
}
|
|
||||||
this.$cache.local.set(
|
|
||||||
"layout-setting",
|
|
||||||
`{
|
|
||||||
"navType":${this.navType},
|
|
||||||
"tagsView":${this.tagsView},
|
|
||||||
"tagsIcon":${this.tagsIcon},
|
|
||||||
"tagsViewStyle":"${this.tagsViewStyle}",
|
|
||||||
"tagsViewPersist":${this.tagsViewPersist},
|
|
||||||
"fixedHeader":${this.fixedHeader},
|
|
||||||
"sidebarLogo":${this.sidebarLogo},
|
|
||||||
"dynamicTitle":${this.dynamicTitle},
|
|
||||||
"footerVisible":${this.footerVisible},
|
|
||||||
"sideTheme":"${this.sideTheme}",
|
|
||||||
"theme":"${this.theme}"
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
setTimeout(this.$modal.closeLoading(), 1000)
|
|
||||||
},
|
|
||||||
resetSetting() {
|
|
||||||
this.$modal.loading("正在清除设置缓存并刷新,请稍候...")
|
|
||||||
this.$cache.local.remove('tags-view-visited')
|
|
||||||
this.$cache.local.remove("layout-setting")
|
|
||||||
setTimeout("window.location.reload()", 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tagsViewPersistChange(val) {
|
||||||
|
settingsStore.tagsViewPersist = val
|
||||||
|
tagsViewPersist.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
function themeChange(val) {
|
||||||
|
settingsStore.theme = val
|
||||||
|
handleThemeStyle(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTheme(val) {
|
||||||
|
settingsStore.sideTheme = val
|
||||||
|
sideTheme.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavType(val) {
|
||||||
|
settingsStore.navType = val
|
||||||
|
navType.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 菜单导航设置 */
|
||||||
|
watch(() => navType, val => {
|
||||||
|
if (val.value == 1) {
|
||||||
|
appStore.sidebar.opened = true
|
||||||
|
appStore.toggleSideBarHide(false)
|
||||||
|
}
|
||||||
|
if (val.value == 2) {
|
||||||
|
appStore.sidebar.opened = true
|
||||||
|
}
|
||||||
|
if (val.value == 3) {
|
||||||
|
appStore.sidebar.opened = false
|
||||||
|
appStore.toggleSideBarHide(true)
|
||||||
|
}
|
||||||
|
if ([1, 3].includes(val.value)) {
|
||||||
|
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function saveSetting() {
|
||||||
|
proxy.$modal.loading("正在保存到本地,请稍候...")
|
||||||
|
if (!tagsViewPersist.value) {
|
||||||
|
proxy.$cache.local.remove('tags-view-visited')
|
||||||
|
}
|
||||||
|
let layoutSetting = {
|
||||||
|
"navType": storeSettings.value.navType,
|
||||||
|
"tagsView": storeSettings.value.tagsView,
|
||||||
|
"tagsIcon": storeSettings.value.tagsIcon,
|
||||||
|
"tagsViewStyle": storeSettings.value.tagsViewStyle,
|
||||||
|
"tagsViewPersist": storeSettings.value.tagsViewPersist,
|
||||||
|
"fixedHeader": storeSettings.value.fixedHeader,
|
||||||
|
"sidebarLogo": storeSettings.value.sidebarLogo,
|
||||||
|
"dynamicTitle": storeSettings.value.dynamicTitle,
|
||||||
|
"footerVisible": storeSettings.value.footerVisible,
|
||||||
|
"sideTheme": storeSettings.value.sideTheme,
|
||||||
|
"theme": storeSettings.value.theme
|
||||||
|
}
|
||||||
|
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting))
|
||||||
|
setTimeout(proxy.$modal.closeLoading(), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSetting() {
|
||||||
|
proxy.$cache.local.remove('tags-view-visited')
|
||||||
|
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...")
|
||||||
|
localStorage.removeItem("layout-setting")
|
||||||
|
setTimeout("window.location.reload()", 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSetting() {
|
||||||
|
showSettings.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openSetting
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
.setting-drawer-content {
|
.setting-drawer-title {
|
||||||
.setting-drawer-title {
|
margin-bottom: 12px;
|
||||||
margin-bottom: 12px;
|
color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
|
||||||
color: rgba(0, 0, 0, .85);
|
line-height: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.setting-drawer-block-checbox {
|
.setting-drawer-block-checbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
.setting-drawer-block-checbox-item {
|
.setting-drawer-block-checbox-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-drawer-block-checbox-selectIcon {
|
.setting-drawer-block-checbox-selectIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-container {
|
.drawer-item {
|
||||||
padding: 20px;
|
color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
|
||||||
|
padding: 12px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
.drawer-title {
|
.comp-style {
|
||||||
margin-bottom: 12px;
|
float: right;
|
||||||
color: rgba(0, 0, 0, .85);
|
margin: -3px 8px 0px 0px;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-item {
|
|
||||||
color: rgba(0, 0, 0, .65);
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-switch {
|
|
||||||
float: right
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +288,7 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
.activeItem {
|
.activeItem {
|
||||||
border: 2px solid #{'var(--theme)'} !important;
|
border: 2px solid var(--el-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@@ -427,4 +341,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export default {
|
|
||||||
computed: {
|
|
||||||
device() {
|
|
||||||
return this.$store.state.app.device
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
|
||||||
this.fixBugIniOS()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fixBugIniOS() {
|
|
||||||
const $subMenu = this.$refs.subMenu
|
|
||||||
if ($subMenu) {
|
|
||||||
const handleMouseleave = $subMenu.handleMouseleave
|
|
||||||
$subMenu.handleMouseleave = (e) => {
|
|
||||||
if (this.device === 'mobile') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleMouseleave(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'MenuItem',
|
|
||||||
functional: true,
|
|
||||||
props: {
|
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render(h, context) {
|
|
||||||
const { icon, title } = context.props
|
|
||||||
const vnodes = []
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
vnodes.push(<svg-icon icon-class={icon}/>)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
if (title.length > 5) {
|
|
||||||
vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
|
|
||||||
} else {
|
|
||||||
vnodes.push(<span slot='title'>{(title)}</span>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vnodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,43 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="type" v-bind="linkProps(to)">
|
<component :is="type" v-bind="linkProps()">
|
||||||
<slot />
|
<slot />
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { isExternal } from '@/utils/validate'
|
import { isExternal } from '@/utils/validate'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
to: {
|
||||||
to: {
|
type: [String, Object],
|
||||||
type: [String, Object],
|
required: true
|
||||||
required: true
|
}
|
||||||
}
|
})
|
||||||
},
|
|
||||||
computed: {
|
const isExt = computed(() => {
|
||||||
isExternal() {
|
return isExternal(props.to)
|
||||||
return isExternal(this.to)
|
})
|
||||||
},
|
|
||||||
type() {
|
const type = computed(() => {
|
||||||
if (this.isExternal) {
|
if (isExt.value) {
|
||||||
return 'a'
|
return 'a'
|
||||||
}
|
}
|
||||||
return 'router-link'
|
return 'router-link'
|
||||||
}
|
})
|
||||||
},
|
|
||||||
methods: {
|
function linkProps() {
|
||||||
linkProps(to) {
|
if (isExt.value) {
|
||||||
if (this.isExternal) {
|
return {
|
||||||
return {
|
href: props.to,
|
||||||
href: to,
|
target: '_blank',
|
||||||
target: '_blank',
|
rel: 'noopener'
|
||||||
rel: 'noopener'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
to: to
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
to: props.to
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,48 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' && navType !== 3 ? variables.menuBackground : variables.menuLightBackground }">
|
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
|
||||||
<transition name="sidebarLogoFade">
|
<transition name="sidebarLogoFade">
|
||||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||||
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
<h1 v-else class="sidebar-title">{{ title }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||||
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
<h1 class="sidebar-title">{{ title }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import logoImg from '@/assets/logo/logo.png'
|
import logo from '@/assets/logo/logo.png'
|
||||||
import variables from '@/assets/styles/variables.scss'
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import variables from '@/assets/styles/variables.module.scss'
|
||||||
|
|
||||||
export default {
|
defineProps({
|
||||||
name: 'SidebarLogo',
|
collapse: {
|
||||||
props: {
|
type: Boolean,
|
||||||
collapse: {
|
required: true
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
variables() {
|
|
||||||
return variables
|
|
||||||
},
|
|
||||||
sideTheme() {
|
|
||||||
return this.$store.state.settings.sideTheme
|
|
||||||
},
|
|
||||||
navType() {
|
|
||||||
return this.$store.state.settings.navType
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
title: process.env.VUE_APP_TITLE,
|
|
||||||
logo: logoImg
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const title = import.meta.env.VITE_APP_TITLE
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const sideTheme = computed(() => settingsStore.sideTheme)
|
||||||
|
|
||||||
|
// 获取Logo背景色
|
||||||
|
const getLogoBackground = computed(() => {
|
||||||
|
if (settingsStore.isDark) {
|
||||||
|
return 'var(--sidebar-bg)'
|
||||||
|
}
|
||||||
|
if (settingsStore.navType == 3) {
|
||||||
|
return variables.menuLightBg
|
||||||
|
}
|
||||||
|
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取Logo文字颜色
|
||||||
|
const getLogoTextColor = computed(() => {
|
||||||
|
if (settingsStore.isDark) {
|
||||||
|
return 'var(--sidebar-logo-text)'
|
||||||
|
}
|
||||||
|
if (settingsStore.navType == 3) {
|
||||||
|
return variables.menuLightText
|
||||||
|
}
|
||||||
|
return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -59,7 +66,7 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
background: #2b2f3a;
|
background: v-bind(getLogoBackground);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -77,7 +84,7 @@ export default {
|
|||||||
& .sidebar-title {
|
& .sidebar-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #fff;
|
color: v-bind(getLogoTextColor);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -92,4 +99,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!item.hidden">
|
<div v-if="!item.hidden">
|
||||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
|
||||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
||||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
|
||||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
|
||||||
|
<template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</app-link>
|
</app-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
|
||||||
<template slot="title">
|
<template v-if="item.meta" #title>
|
||||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
<svg-icon :icon-class="item.meta && item.meta.icon" />
|
||||||
|
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<sidebar-item
|
<sidebar-item
|
||||||
v-for="(child, index) in item.children"
|
v-for="(child, index) in item.children"
|
||||||
:key="child.path + index"
|
:key="child.path + index"
|
||||||
@@ -20,80 +23,78 @@
|
|||||||
:base-path="resolvePath(child.path)"
|
:base-path="resolvePath(child.path)"
|
||||||
class="nest-menu"
|
class="nest-menu"
|
||||||
/>
|
/>
|
||||||
</el-submenu>
|
</el-sub-menu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import path from 'path'
|
|
||||||
import { isExternal } from '@/utils/validate'
|
import { isExternal } from '@/utils/validate'
|
||||||
import Item from './Item'
|
|
||||||
import AppLink from './Link'
|
import AppLink from './Link'
|
||||||
import FixiOSBug from './FixiOSBug'
|
import { getNormalPath } from '@/utils/ruoyi'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'SidebarItem',
|
// route object
|
||||||
components: { Item, AppLink },
|
item: {
|
||||||
mixins: [FixiOSBug],
|
type: Object,
|
||||||
props: {
|
required: true
|
||||||
// route object
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
isNest: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
basePath: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
isNest: {
|
||||||
this.onlyOneChild = null
|
type: Boolean,
|
||||||
return {}
|
default: false
|
||||||
},
|
},
|
||||||
methods: {
|
basePath: {
|
||||||
hasOneShowingChild(children = [], parent) {
|
type: String,
|
||||||
if (!children) {
|
default: ''
|
||||||
children = []
|
}
|
||||||
}
|
})
|
||||||
const showingChildren = children.filter(item => {
|
|
||||||
if (item.hidden) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Temp set(will be used if only has one showing child)
|
|
||||||
this.onlyOneChild = item
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// When there is only one child router, the child router is displayed by default
|
const onlyOneChild = ref({})
|
||||||
if (showingChildren.length === 1) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show parent if there are no child router to display
|
|
||||||
if (showingChildren.length === 0) {
|
|
||||||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function hasOneShowingChild(children = [], parent) {
|
||||||
|
if (!children) {
|
||||||
|
children = []
|
||||||
|
}
|
||||||
|
const showingChildren = children.filter(item => {
|
||||||
|
if (item.hidden) {
|
||||||
return false
|
return false
|
||||||
},
|
|
||||||
resolvePath(routePath, routeQuery) {
|
|
||||||
if (isExternal(routePath)) {
|
|
||||||
return routePath
|
|
||||||
}
|
|
||||||
if (isExternal(this.basePath)) {
|
|
||||||
return this.basePath
|
|
||||||
}
|
|
||||||
if (routeQuery) {
|
|
||||||
let query = JSON.parse(routeQuery)
|
|
||||||
return { path: path.resolve(this.basePath, routePath), query: query }
|
|
||||||
}
|
|
||||||
return path.resolve(this.basePath, routePath)
|
|
||||||
}
|
}
|
||||||
|
onlyOneChild.value = item
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// When there is only one child router, the child router is displayed by default
|
||||||
|
if (showingChildren.length === 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show parent if there are no child router to display
|
||||||
|
if (showingChildren.length === 0) {
|
||||||
|
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(routePath, routeQuery) {
|
||||||
|
if (isExternal(routePath)) {
|
||||||
|
return routePath
|
||||||
|
}
|
||||||
|
if (isExternal(props.basePath)) {
|
||||||
|
return props.basePath
|
||||||
|
}
|
||||||
|
if (routeQuery) {
|
||||||
|
let query = JSON.parse(routeQuery)
|
||||||
|
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
|
||||||
|
}
|
||||||
|
return getNormalPath(props.basePath + '/' + routePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTitle(title){
|
||||||
|
if (title.length > 5) {
|
||||||
|
return title
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,57 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['sidebar-theme-wrapper', {'has-logo':showLogo}, settings.sideTheme]" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
<div :class="['sidebar-theme-wrapper', {'has-logo':showLogo}, sideTheme]" class="sidebar-container">
|
||||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||||
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
|
:background-color="getMenuBackground"
|
||||||
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
:text-color="getMenuTextColor"
|
||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
:active-text-color="settings.theme"
|
:active-text-color="theme"
|
||||||
:collapse-transition="false"
|
:collapse-transition="false"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
>
|
:class="sideTheme"
|
||||||
<sidebar-item
|
>
|
||||||
v-for="(route, index) in sidebarRouters"
|
<sidebar-item
|
||||||
:key="route.path + index"
|
v-for="(route, index) in sidebarRouters"
|
||||||
:item="route"
|
:key="route.path + index"
|
||||||
:base-path="route.path"
|
:item="route"
|
||||||
/>
|
:base-path="route.path"
|
||||||
</el-menu>
|
/>
|
||||||
</el-scrollbar>
|
</el-menu>
|
||||||
</div>
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { mapGetters, mapState } from "vuex"
|
import Logo from './Logo'
|
||||||
import Logo from "./Logo"
|
import SidebarItem from './SidebarItem'
|
||||||
import SidebarItem from "./SidebarItem"
|
import variables from '@/assets/styles/variables.module.scss'
|
||||||
import variables from "@/assets/styles/variables.scss"
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
components: { SidebarItem, Logo },
|
const appStore = useAppStore()
|
||||||
computed: {
|
const settingsStore = useSettingsStore()
|
||||||
...mapState(["settings"]),
|
const permissionStore = usePermissionStore()
|
||||||
...mapGetters(["sidebarRouters", "sidebar"]),
|
|
||||||
activeMenu() {
|
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
|
||||||
const route = this.$route
|
const showLogo = computed(() => settingsStore.sidebarLogo)
|
||||||
const { meta, path } = route
|
const sideTheme = computed(() => settingsStore.sideTheme)
|
||||||
// if set path, the sidebar will highlight the path you set
|
const theme = computed(() => settingsStore.theme)
|
||||||
if (meta.activeMenu) {
|
const isCollapse = computed(() => !appStore.sidebar.opened)
|
||||||
return meta.activeMenu
|
|
||||||
}
|
// 获取菜单背景色
|
||||||
return path
|
const getMenuBackground = computed(() => {
|
||||||
},
|
if (settingsStore.isDark) {
|
||||||
showLogo() {
|
return 'var(--sidebar-bg)'
|
||||||
return this.$store.state.settings.sidebarLogo
|
}
|
||||||
},
|
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
|
||||||
variables() {
|
})
|
||||||
return variables
|
|
||||||
},
|
// 获取菜单文字颜色
|
||||||
isCollapse() {
|
const getMenuTextColor = computed(() => {
|
||||||
return !this.sidebar.opened
|
if (settingsStore.isDark) {
|
||||||
}
|
return 'var(--sidebar-text)'
|
||||||
}
|
}
|
||||||
}
|
return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const { meta, path } = route
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
return meta.activeMenu
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-container {
|
||||||
|
background-color: v-bind(getMenuBackground);
|
||||||
|
|
||||||
|
.scrollbar-wrapper {
|
||||||
|
background-color: v-bind(getMenuBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
.el-menu-item, .el-sub-menu__title {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
color: v-bind(getMenuTextColor);
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--menu-active-text, #409eff);
|
||||||
|
background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: v-bind(getMenuTextColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,149 +1,156 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
<el-scrollbar
|
||||||
|
ref="scrollContainer"
|
||||||
|
:vertical="false"
|
||||||
|
class="scroll-container"
|
||||||
|
@wheel.prevent="handleScroll"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
const tagAndTagSpacing = 4
|
import useTagsViewStore from '@/store/modules/tagsView'
|
||||||
|
|
||||||
export default {
|
const tagAndTagSpacing = ref(4)
|
||||||
name: 'ScrollPane',
|
const { proxy } = getCurrentInstance()
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
left: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
scrollWrapper() {
|
|
||||||
return this.$refs.scrollContainer.$refs.wrap
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
smoothScrollTo(target) {
|
|
||||||
const $scrollWrapper = this.scrollWrapper
|
|
||||||
const start = $scrollWrapper.scrollLeft
|
|
||||||
const distance = target - start
|
|
||||||
const duration = 300
|
|
||||||
let startTime = null
|
|
||||||
|
|
||||||
function ease(t, b, c, d) {
|
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
|
||||||
t /= d / 2
|
|
||||||
if (t < 1) return c / 2 * t * t + b
|
|
||||||
t--
|
|
||||||
return -c / 2 * (t * (t - 2) - 1) + b
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = this.$emit.bind(this)
|
const emits = defineEmits(['scroll', 'updateArrows'])
|
||||||
function step(timestamp) {
|
|
||||||
if (!startTime) startTime = timestamp
|
|
||||||
const elapsed = timestamp - startTime
|
|
||||||
$scrollWrapper.scrollLeft = ease(elapsed, start, distance, duration)
|
|
||||||
if (elapsed < duration) {
|
|
||||||
requestAnimationFrame(step)
|
|
||||||
} else {
|
|
||||||
$scrollWrapper.scrollLeft = target
|
|
||||||
emit('updateArrows')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
scrollWrapper.value.removeEventListener('scroll', emitScroll)
|
||||||
|
})
|
||||||
|
|
||||||
|
const emitScroll = () => {
|
||||||
|
emits('scroll')
|
||||||
|
emits('updateArrows')
|
||||||
|
}
|
||||||
|
|
||||||
|
function smoothScrollTo(target) {
|
||||||
|
const $scrollWrapper = scrollWrapper.value
|
||||||
|
const start = $scrollWrapper.scrollLeft
|
||||||
|
const distance = target - start
|
||||||
|
const duration = 300
|
||||||
|
let startTime = null
|
||||||
|
|
||||||
|
function ease(t, b, c, d) {
|
||||||
|
t /= d / 2
|
||||||
|
if (t < 1) return c / 2 * t * t + b
|
||||||
|
t--
|
||||||
|
return -c / 2 * (t * (t - 2) - 1) + b
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(timestamp) {
|
||||||
|
if (!startTime) startTime = timestamp
|
||||||
|
const elapsed = timestamp - startTime
|
||||||
|
$scrollWrapper.scrollLeft = ease(elapsed, start, distance, duration)
|
||||||
|
if (elapsed < duration) {
|
||||||
requestAnimationFrame(step)
|
requestAnimationFrame(step)
|
||||||
},
|
} else {
|
||||||
handleScroll(e) {
|
$scrollWrapper.scrollLeft = target
|
||||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
emits('updateArrows')
|
||||||
const $scrollWrapper = this.scrollWrapper
|
}
|
||||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
}
|
||||||
this.$emit('updateArrows')
|
|
||||||
},
|
|
||||||
emitScroll() {
|
|
||||||
this.$emit('scroll')
|
|
||||||
this.$emit('updateArrows')
|
|
||||||
},
|
|
||||||
moveToTarget(currentTag) {
|
|
||||||
const $container = this.$refs.scrollContainer.$el
|
|
||||||
const $containerWidth = $container.offsetWidth
|
|
||||||
const $scrollWrapper = this.scrollWrapper
|
|
||||||
const tagList = this.$parent.$refs.tag
|
|
||||||
|
|
||||||
let firstTag = null
|
requestAnimationFrame(step)
|
||||||
let lastTag = null
|
}
|
||||||
|
|
||||||
if (tagList.length > 0) {
|
function handleScroll(e) {
|
||||||
firstTag = tagList[0]
|
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||||
lastTag = tagList[tagList.length - 1]
|
const $scrollWrapper = scrollWrapper.value
|
||||||
}
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||||
|
emits('updateArrows')
|
||||||
|
}
|
||||||
|
|
||||||
if (firstTag === currentTag) {
|
const tagsViewStore = useTagsViewStore()
|
||||||
this.smoothScrollTo(0)
|
const visitedViews = computed(() => tagsViewStore.visitedViews)
|
||||||
} else if (lastTag === currentTag) {
|
|
||||||
this.smoothScrollTo($scrollWrapper.scrollWidth - $containerWidth)
|
|
||||||
} else {
|
|
||||||
const currentIndex = tagList.findIndex(item => item === currentTag)
|
|
||||||
const prevTag = tagList[currentIndex - 1]
|
|
||||||
const nextTag = tagList[currentIndex + 1]
|
|
||||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
|
||||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
|
||||||
|
|
||||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
function moveToTarget(currentTag) {
|
||||||
this.smoothScrollTo(afterNextTagOffsetLeft - $containerWidth)
|
const $container = proxy.$refs.scrollContainer.$el
|
||||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
const $containerWidth = $container.offsetWidth
|
||||||
this.smoothScrollTo(beforePrevTagOffsetLeft)
|
const $scrollWrapper = scrollWrapper.value
|
||||||
|
|
||||||
|
let firstTag = null
|
||||||
|
let lastTag = null
|
||||||
|
|
||||||
|
if (visitedViews.value.length > 0) {
|
||||||
|
firstTag = visitedViews.value[0]
|
||||||
|
lastTag = visitedViews.value[visitedViews.value.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTag === currentTag) {
|
||||||
|
smoothScrollTo(0)
|
||||||
|
} else if (lastTag === currentTag) {
|
||||||
|
smoothScrollTo($scrollWrapper.scrollWidth - $containerWidth)
|
||||||
|
} else {
|
||||||
|
const tagListDom = document.getElementsByClassName('tags-view-item')
|
||||||
|
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
|
||||||
|
let prevTag = null
|
||||||
|
let nextTag = null
|
||||||
|
for (const k in tagListDom) {
|
||||||
|
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||||
|
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
|
||||||
|
prevTag = tagListDom[k]
|
||||||
|
}
|
||||||
|
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
|
||||||
|
nextTag = tagListDom[k]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// 向左滚动固定距离
|
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
|
||||||
scrollLeft() {
|
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
|
||||||
const $scrollWrapper = this.scrollWrapper
|
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||||
this.smoothScrollTo(Math.max(0, $scrollWrapper.scrollLeft - 200))
|
smoothScrollTo(afterNextTagOffsetLeft - $containerWidth)
|
||||||
},
|
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||||
// 向右滚动固定距离
|
smoothScrollTo(beforePrevTagOffsetLeft)
|
||||||
scrollRight() {
|
|
||||||
const $scrollWrapper = this.scrollWrapper
|
|
||||||
const maxScroll = $scrollWrapper.scrollWidth - $scrollWrapper.clientWidth
|
|
||||||
this.smoothScrollTo(Math.min(maxScroll, $scrollWrapper.scrollLeft + 200))
|
|
||||||
},
|
|
||||||
// 直接平滑滚动到最左端
|
|
||||||
scrollToStart() {
|
|
||||||
this.smoothScrollTo(0)
|
|
||||||
},
|
|
||||||
// 直接平滑滚动到最右端
|
|
||||||
scrollToEnd() {
|
|
||||||
const $scrollWrapper = this.scrollWrapper
|
|
||||||
this.smoothScrollTo($scrollWrapper.scrollWidth - $scrollWrapper.clientWidth)
|
|
||||||
},
|
|
||||||
// 获取是否可以继续向左/右滚动
|
|
||||||
getScrollState() {
|
|
||||||
const $scrollWrapper = this.scrollWrapper
|
|
||||||
return {
|
|
||||||
canLeft: $scrollWrapper.scrollLeft > 0,
|
|
||||||
canRight: $scrollWrapper.scrollLeft < $scrollWrapper.scrollWidth - $scrollWrapper.clientWidth - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToStart() {
|
||||||
|
smoothScrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToEnd() {
|
||||||
|
const $scrollWrapper = scrollWrapper.value
|
||||||
|
smoothScrollTo($scrollWrapper.scrollWidth - $scrollWrapper.clientWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollState() {
|
||||||
|
const $scrollWrapper = scrollWrapper.value
|
||||||
|
return {
|
||||||
|
canLeft: $scrollWrapper.scrollLeft > 0,
|
||||||
|
canRight: $scrollWrapper.scrollLeft < $scrollWrapper.scrollWidth - $scrollWrapper.clientWidth - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
moveToTarget,
|
||||||
|
scrollToStart,
|
||||||
|
scrollToEnd,
|
||||||
|
getScrollState
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
.scroll-container {
|
.scroll-container {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
::v-deep {
|
:deep(.el-scrollbar__bar) {
|
||||||
.el-scrollbar__bar {
|
bottom: 0px;
|
||||||
bottom: 0px;
|
}
|
||||||
}
|
:deep(.el-scrollbar__wrap) {
|
||||||
.el-scrollbar__wrap {
|
height: 34px;
|
||||||
height: 34px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,391 +1,372 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="tags-view-container" class="tags-view-container" :class="{ 'tags-view-container--chrome': tagsViewStyle === 'chrome' }" :style="chromeVars">
|
<div id="tags-view-container" class="tags-view-container" :class="{ 'tags-view-container--chrome': tagsViewStyle === 'chrome' }">
|
||||||
<!-- 左切换箭头 -->
|
<!-- 左切换箭头 -->
|
||||||
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
|
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
|
||||||
<i class="el-icon-arrow-left" />
|
<el-icon><arrow-left /></el-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 标签滚动区 -->
|
<!-- 标签滚动区 -->
|
||||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll" @updateArrows="updateArrowState">
|
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll" @update-arrows="updateArrowState">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tag in visitedViews"
|
v-for="tag in visitedViews"
|
||||||
ref="tag"
|
|
||||||
:key="tag.path"
|
:key="tag.path"
|
||||||
|
:data-path="tag.path"
|
||||||
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
|
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
|
||||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||||
tag="span"
|
|
||||||
class="tags-view-item"
|
class="tags-view-item"
|
||||||
:style="tagActiveStyle(tag)"
|
:style="tagActiveStyle(tag)"
|
||||||
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||||
@contextmenu.prevent.native="openMenu(tag, $event)"
|
@contextmenu.prevent="openMenu(tag, $event)"
|
||||||
>
|
>
|
||||||
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" style="margin-right: 3px;" />
|
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" style="margin-right: 3px;" />
|
||||||
{{ tag.title }}
|
{{ tag.title }}
|
||||||
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)" class="tags-close-btn">
|
||||||
|
<close class="el-icon-close" />
|
||||||
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</scroll-pane>
|
</scroll-pane>
|
||||||
|
|
||||||
<!-- 右切换箭头 -->
|
<!-- 右切换箭头 -->
|
||||||
<span class="tags-nav-btn tags-nav-btn--right" :class="{ disabled: !canScrollRight }" @click="scrollRight">
|
<span class="tags-nav-btn tags-nav-btn--right" :class="{ disabled: !canScrollRight }" @click="scrollRight">
|
||||||
<i class="el-icon-arrow-right" />
|
<el-icon><arrow-right /></el-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 下拉操作菜单 -->
|
<!-- 下拉操作菜单 -->
|
||||||
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
|
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
|
||||||
<span class="tags-action-btn">
|
<span class="tags-action-btn">
|
||||||
<i class="el-icon-arrow-down" />
|
<el-icon><arrow-down /></el-icon>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown" class="tags-dropdown-menu">
|
<template #dropdown>
|
||||||
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close" icon="el-icon-close">关闭当前</el-dropdown-item>
|
<el-dropdown-menu class="tags-dropdown-menu">
|
||||||
<el-dropdown-item command="closeOthers" icon="el-icon-circle-close">关闭其他</el-dropdown-item>
|
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close"><close style="width: 1em; height: 1em;" />关闭当前</el-dropdown-item>
|
||||||
<el-dropdown-item command="closeLeft" :disabled="isFirstView()" icon="el-icon-back">关闭左侧</el-dropdown-item>
|
<el-dropdown-item command="closeOthers"><circle-close style="width: 1em; height: 1em;" />关闭其他</el-dropdown-item>
|
||||||
<el-dropdown-item command="closeRight" :disabled="isLastView()" icon="el-icon-right">关闭右侧</el-dropdown-item>
|
<el-dropdown-item command="closeLeft" :disabled="isFirstView()"><back style="width: 1em; height: 1em;" />关闭左侧</el-dropdown-item>
|
||||||
<el-dropdown-item command="closeAll" icon="el-icon-circle-close">全部关闭</el-dropdown-item>
|
<el-dropdown-item command="closeRight" :disabled="isLastView()"><right style="width: 1em; height: 1em;" />关闭右侧</el-dropdown-item>
|
||||||
<el-dropdown-item command="fullscreen" divided>
|
<el-dropdown-item command="closeAll"><circle-close style="width: 1em; height: 1em;" />全部关闭</el-dropdown-item>
|
||||||
<template v-if="!isFullscreen"><i class="el-icon-full-screen"></i>全屏显示</template>
|
<el-dropdown-item command="fullscreen" divided>
|
||||||
<template v-else><i class="el-icon-close"></i>退出全屏</template>
|
<template v-if="!isFullscreen"><full-screen style="width: 1em; height: 1em;" />全屏显示</template>
|
||||||
</el-dropdown-item>
|
<template v-else><close style="width: 1em; height: 1em;" />退出全屏</template>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 刷新按钮 -->
|
||||||
<span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)">
|
<span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)">
|
||||||
<i class="el-icon-refresh-right" /> 刷新
|
<el-icon><refresh-right/></el-icon> 刷新
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 右键上下文菜单 -->
|
<!-- 右键上下文菜单 -->
|
||||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||||
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li>
|
<li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em;" />刷新页面</li>
|
||||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em;" />关闭当前</li>
|
||||||
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
|
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em;" />关闭其他</li>
|
||||||
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li>
|
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em;" />关闭左侧</li>
|
||||||
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li>
|
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em;" />关闭右侧</li>
|
||||||
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li>
|
<li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em;" />全部关闭</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ScrollPane from './ScrollPane'
|
import ScrollPane from './ScrollPane'
|
||||||
import path from 'path'
|
import { getNormalPath } from '@/utils/ruoyi'
|
||||||
|
import useTagsViewStore from '@/store/modules/tagsView'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
export default {
|
const visible = ref(false)
|
||||||
components: { ScrollPane },
|
const top = ref(0)
|
||||||
data() {
|
const left = ref(0)
|
||||||
return {
|
const selectedTag = ref({})
|
||||||
visible: false,
|
const affixTags = ref([])
|
||||||
top: 0,
|
const scrollPaneRef = ref(null)
|
||||||
left: 0,
|
const canScrollLeft = ref(false)
|
||||||
selectedTag: {},
|
const canScrollRight = ref(false)
|
||||||
affixTags: [],
|
const isFullscreen = ref(false)
|
||||||
canScrollLeft: false,
|
const hiddenElements = ref([])
|
||||||
canScrollRight: false,
|
|
||||||
isFullscreen: false,
|
|
||||||
hiddenElements: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
visitedViews() {
|
|
||||||
return this.$store.state.tagsView.visitedViews
|
|
||||||
},
|
|
||||||
routes() {
|
|
||||||
return this.$store.state.permission.routes
|
|
||||||
},
|
|
||||||
theme() {
|
|
||||||
return this.$store.state.settings.theme
|
|
||||||
},
|
|
||||||
tagsIcon() {
|
|
||||||
return this.$store.state.settings.tagsIcon
|
|
||||||
},
|
|
||||||
tagsViewStyle() {
|
|
||||||
return this.$store.state.settings.tagsViewStyle
|
|
||||||
},
|
|
||||||
selectedDropdownTag() {
|
|
||||||
return this.visitedViews.find(v => this.isActive(v)) || {}
|
|
||||||
},
|
|
||||||
chromeVars() {
|
|
||||||
if (this.tagsViewStyle !== 'chrome') return {}
|
|
||||||
const primary = this.theme || '#409EFF'
|
|
||||||
return {
|
|
||||||
'--chrome-tab-active-bg': this.mixHexWithWhite(primary, 0.15),
|
|
||||||
'--chrome-tab-text-active': primary,
|
|
||||||
'--chrome-wing-r': '14px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route() {
|
|
||||||
this.addTags()
|
|
||||||
this.moveToCurrentTag()
|
|
||||||
},
|
|
||||||
visible(value) {
|
|
||||||
if (value) {
|
|
||||||
document.body.addEventListener('click', this.closeMenu)
|
|
||||||
} else {
|
|
||||||
document.body.removeEventListener('click', this.closeMenu)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
visitedViews() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateArrowState()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.initTags()
|
|
||||||
this.addTags()
|
|
||||||
window.addEventListener('resize', this.updateArrowState)
|
|
||||||
window.addEventListener('keydown', this.handleKeyDown)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('resize', this.updateArrowState)
|
|
||||||
window.removeEventListener('keydown', this.handleKeyDown)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleKeyDown(event) {
|
|
||||||
// 当按下Esc键且处于全屏状态时,退出全屏
|
|
||||||
if (event.key === 'Escape' && this.isFullscreen) {
|
|
||||||
this.toggleFullscreen()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mixHexWithWhite(hex, ratio) {
|
|
||||||
const clean = hex.replace('#', '')
|
|
||||||
const r = parseInt(clean.substring(0, 2), 16)
|
|
||||||
const g = parseInt(clean.substring(2, 4), 16)
|
|
||||||
const b = parseInt(clean.substring(4, 6), 16)
|
|
||||||
const mr = Math.round(r * ratio + 255 * (1 - ratio))
|
|
||||||
const mg = Math.round(g * ratio + 255 * (1 - ratio))
|
|
||||||
const mb = Math.round(b * ratio + 255 * (1 - ratio))
|
|
||||||
return `rgb(${mr}, ${mg}, ${mb})`
|
|
||||||
},
|
|
||||||
isActive(route) {
|
|
||||||
return route.path === this.$route.path
|
|
||||||
},
|
|
||||||
tagActiveStyle(tag) {
|
|
||||||
if (!this.isActive(tag) || this.tagsViewStyle !== 'card') return {}
|
|
||||||
return {
|
|
||||||
"background-color": this.theme,
|
|
||||||
"border-color": this.theme
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isAffix(tag) {
|
|
||||||
return tag && tag.meta && tag.meta.affix
|
|
||||||
},
|
|
||||||
isFirstView() {
|
|
||||||
try {
|
|
||||||
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
|
|
||||||
return tag.fullPath === '/index' || tag.fullPath === this.visitedViews[1].fullPath
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isLastView() {
|
|
||||||
try {
|
|
||||||
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
|
|
||||||
return tag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filterAffixTags(routes, basePath = '/') {
|
|
||||||
let tags = []
|
|
||||||
routes.forEach(route => {
|
|
||||||
if (route.meta && route.meta.affix) {
|
|
||||||
const tagPath = path.resolve(basePath, route.path)
|
|
||||||
tags.push({
|
|
||||||
fullPath: tagPath,
|
|
||||||
path: tagPath,
|
|
||||||
name: route.name,
|
|
||||||
meta: { ...route.meta }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (route.children) {
|
|
||||||
const tempTags = this.filterAffixTags(route.children, route.path)
|
|
||||||
if (tempTags.length >= 1) {
|
|
||||||
tags = [...tags, ...tempTags]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return tags
|
|
||||||
},
|
|
||||||
initTags() {
|
|
||||||
if (this.$store.state.settings.tagsViewPersist) {
|
|
||||||
this.$store.dispatch('tagsView/loadPersistedViews')
|
|
||||||
}
|
|
||||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
|
||||||
for (const tag of affixTags) {
|
|
||||||
if (tag.name) {
|
|
||||||
this.$store.dispatch('tagsView/addAffixView', tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addTags() {
|
|
||||||
const { name } = this.$route
|
|
||||||
if (name) {
|
|
||||||
this.$store.dispatch('tagsView/addView', this.$route)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moveToCurrentTag() {
|
|
||||||
const tags = this.$refs.tag
|
|
||||||
this.$nextTick(() => {
|
|
||||||
for (const tag of tags) {
|
|
||||||
if (tag.to.path === this.$route.path) {
|
|
||||||
this.$refs.scrollPane.moveToTarget(tag)
|
|
||||||
if (tag.to.fullPath !== this.$route.fullPath) {
|
|
||||||
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
scrollLeft() {
|
|
||||||
if (!this.canScrollLeft) return
|
|
||||||
this.$refs.scrollPane.scrollToStart()
|
|
||||||
},
|
|
||||||
scrollRight() {
|
|
||||||
if (!this.canScrollRight) return
|
|
||||||
this.$refs.scrollPane.scrollToEnd()
|
|
||||||
},
|
|
||||||
updateArrowState() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.scrollPane) {
|
|
||||||
const state = this.$refs.scrollPane.getScrollState()
|
|
||||||
this.canScrollLeft = state.canLeft
|
|
||||||
this.canScrollRight = state.canRight
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toggleFullscreen() {
|
|
||||||
const mainContainer = document.querySelector('.main-container')
|
|
||||||
const navbar = document.querySelector('.navbar')
|
|
||||||
const sidebar = document.querySelector('.sidebar-container')
|
|
||||||
if (!mainContainer) return
|
|
||||||
|
|
||||||
if (!this.isFullscreen) {
|
const { proxy } = getCurrentInstance()
|
||||||
mainContainer.classList.add('fullscreen-mode')
|
const route = useRoute()
|
||||||
document.body.style.overflow = 'hidden'
|
const router = useRouter()
|
||||||
const elementsToHide = [
|
const settingsStore = useSettingsStore()
|
||||||
{ el: navbar, originalDisplay: (navbar && navbar.style.display) || '' },
|
|
||||||
{ el: sidebar, originalDisplay: (sidebar && sidebar.style.display) || '' }
|
const visitedViews = computed(() => useTagsViewStore().visitedViews)
|
||||||
]
|
const routes = computed(() => usePermissionStore().routes)
|
||||||
elementsToHide.forEach(item => {
|
const theme = computed(() => useSettingsStore().theme)
|
||||||
if (item.el && item.el.style.display !== 'none') {
|
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
|
||||||
item.originalDisplay = item.el.style.display
|
const tagsViewPersist = computed(() => useSettingsStore().tagsViewPersist)
|
||||||
item.el.style.display = 'none'
|
const tagsViewStyle = computed(() => useSettingsStore().tagsViewStyle)
|
||||||
this.hiddenElements.push(item)
|
|
||||||
}
|
// 下拉菜单针对当前激活的 tag
|
||||||
})
|
const selectedDropdownTag = computed(() => visitedViews.value.find(v => isActive(v)) || {})
|
||||||
this.isFullscreen = true
|
|
||||||
} else {
|
watch(route, () => {
|
||||||
mainContainer.classList.remove('fullscreen-mode')
|
addTags()
|
||||||
document.body.style.overflow = ''
|
moveToCurrentTag()
|
||||||
this.hiddenElements.forEach(item => {
|
})
|
||||||
if (item.el) {
|
|
||||||
item.el.style.display = item.originalDisplay
|
watch(visible, (value) => {
|
||||||
}
|
if (value) {
|
||||||
})
|
document.body.addEventListener('click', closeMenu)
|
||||||
this.hiddenElements = []
|
} else {
|
||||||
const btn = document.querySelector('.tags-action-btn')
|
document.body.removeEventListener('click', closeMenu)
|
||||||
if (btn) btn.blur()
|
}
|
||||||
this.isFullscreen = false
|
})
|
||||||
|
|
||||||
|
watch(visitedViews, () => {
|
||||||
|
nextTick(() => updateArrowState())
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initTags()
|
||||||
|
addTags()
|
||||||
|
window.addEventListener('resize', updateArrowState)
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateArrowState)
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
// 当按下Esc键且处于全屏状态时,退出全屏
|
||||||
|
if (event.key === 'Escape' && isFullscreen.value) {
|
||||||
|
toggleFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(r) {
|
||||||
|
return r.path === route.path
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagActiveStyle(tag) {
|
||||||
|
if (!isActive(tag) || tagsViewStyle.value !== 'card') return {}
|
||||||
|
return {
|
||||||
|
'background-color': theme.value,
|
||||||
|
'border-color': theme.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAffix(tag) {
|
||||||
|
return tag && tag.meta && tag.meta.affix
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFirstView() {
|
||||||
|
try {
|
||||||
|
const tag = selectedTag.value && selectedTag.value.fullPath ? selectedTag.value : selectedDropdownTag.value
|
||||||
|
return tag.fullPath === '/index' || tag.fullPath === visitedViews.value[1].fullPath
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLastView() {
|
||||||
|
try {
|
||||||
|
const tag = selectedTag.value && selectedTag.value.fullPath ? selectedTag.value : selectedDropdownTag.value
|
||||||
|
return tag.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAffixTags(routes, basePath = '') {
|
||||||
|
let tags = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.meta && route.meta.affix) {
|
||||||
|
const tagPath = getNormalPath(basePath + '/' + route.path)
|
||||||
|
tags.push({
|
||||||
|
fullPath: tagPath,
|
||||||
|
path: tagPath,
|
||||||
|
name: route.name,
|
||||||
|
meta: { ...route.meta }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (route.children) {
|
||||||
|
const tempTags = filterAffixTags(route.children, route.path)
|
||||||
|
if (tempTags.length >= 1) {
|
||||||
|
tags = [...tags, ...tempTags]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
handleDropdownCommand(command) {
|
})
|
||||||
const tag = this.selectedDropdownTag
|
return tags
|
||||||
this.selectedTag = tag
|
}
|
||||||
switch (command) {
|
|
||||||
case 'refresh':
|
function initTags() {
|
||||||
this.refreshSelectedTag(tag)
|
if (tagsViewPersist.value) {
|
||||||
break
|
useTagsViewStore().loadPersistedViews()
|
||||||
case 'fullscreen':
|
}
|
||||||
this.toggleFullscreen()
|
const res = filterAffixTags(routes.value)
|
||||||
break
|
affixTags.value = res
|
||||||
case 'close':
|
for (const tag of res) {
|
||||||
this.closeSelectedTag(tag)
|
if (tag.name) {
|
||||||
break
|
useTagsViewStore().addAffixView(tag)
|
||||||
case 'closeOthers':
|
|
||||||
this.closeOthersTags()
|
|
||||||
break
|
|
||||||
case 'closeLeft':
|
|
||||||
this.closeLeftTags()
|
|
||||||
break
|
|
||||||
case 'closeRight':
|
|
||||||
this.closeRightTags()
|
|
||||||
break
|
|
||||||
case 'closeAll':
|
|
||||||
this.closeAllTags(tag)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refreshSelectedTag(view) {
|
|
||||||
this.$tab.refreshPage(view)
|
|
||||||
if (this.$route.meta.link) {
|
|
||||||
this.$store.dispatch('tagsView/delIframeView', this.$route)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeSelectedTag(view) {
|
|
||||||
this.$tab.closePage(view).then(({ visitedViews }) => {
|
|
||||||
if (this.isActive(view)) {
|
|
||||||
this.toLastView(visitedViews, view)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeRightTags() {
|
|
||||||
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
|
|
||||||
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
|
|
||||||
this.toLastView(visitedViews)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeLeftTags() {
|
|
||||||
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
|
|
||||||
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
|
|
||||||
this.toLastView(visitedViews)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeOthersTags() {
|
|
||||||
this.$router.push(this.selectedTag.fullPath).catch(() => {})
|
|
||||||
this.$tab.closeOtherPage(this.selectedTag).then(() => {
|
|
||||||
this.moveToCurrentTag()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeAllTags(view) {
|
|
||||||
this.$tab.closeAllPage().then(({ visitedViews }) => {
|
|
||||||
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.toLastView(visitedViews, view)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toLastView(visitedViews, view) {
|
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
|
||||||
if (latestView) {
|
|
||||||
this.$router.push(latestView.fullPath)
|
|
||||||
} else {
|
|
||||||
if (view && view.name === 'Dashboard') {
|
|
||||||
this.$router.replace({ path: '/redirect' + view.fullPath })
|
|
||||||
} else {
|
|
||||||
this.$router.push('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openMenu(tag, e) {
|
|
||||||
this.left = e.clientX
|
|
||||||
this.top = e.clientY
|
|
||||||
this.visible = true
|
|
||||||
this.selectedTag = tag
|
|
||||||
},
|
|
||||||
closeMenu() {
|
|
||||||
this.visible = false
|
|
||||||
},
|
|
||||||
handleScroll() {
|
|
||||||
this.closeMenu()
|
|
||||||
this.updateArrowState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addTags() {
|
||||||
|
const { name } = route
|
||||||
|
if (name) {
|
||||||
|
useTagsViewStore().addView(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToCurrentTag() {
|
||||||
|
nextTick(() => {
|
||||||
|
for (const r of visitedViews.value) {
|
||||||
|
if (r.path === route.path) {
|
||||||
|
scrollPaneRef.value.moveToTarget(r)
|
||||||
|
if (r.fullPath !== route.fullPath) {
|
||||||
|
useTagsViewStore().updateVisitedView(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollLeft() {
|
||||||
|
if (!canScrollLeft.value) return
|
||||||
|
scrollPaneRef.value.scrollToStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollRight() {
|
||||||
|
if (!canScrollRight.value) return
|
||||||
|
scrollPaneRef.value.scrollToEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateArrowState() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (scrollPaneRef.value) {
|
||||||
|
const state = scrollPaneRef.value.getScrollState()
|
||||||
|
canScrollLeft.value = state.canLeft
|
||||||
|
canScrollRight.value = state.canRight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
const mainContainer = document.querySelector('.main-container')
|
||||||
|
const navbar = document.querySelector('.navbar')
|
||||||
|
const sidebar = document.querySelector('.sidebar-container')
|
||||||
|
if (!mainContainer) return
|
||||||
|
|
||||||
|
if (!isFullscreen.value) {
|
||||||
|
mainContainer.classList.add('fullscreen-mode')
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
const elementsToHide = [{ el: navbar, originalDisplay: navbar?.style.display || '' }, { el: sidebar, originalDisplay: sidebar?.style.display || '' }]
|
||||||
|
elementsToHide.forEach(item => {
|
||||||
|
if (item.el && item.el.style.display !== 'none') {
|
||||||
|
item.originalDisplay = item.el.style.display
|
||||||
|
item.el.style.display = 'none'
|
||||||
|
hiddenElements.value.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
isFullscreen.value = true
|
||||||
|
} else {
|
||||||
|
mainContainer.classList.remove('fullscreen-mode')
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
hiddenElements.value.forEach(item => {
|
||||||
|
if (item.el) {
|
||||||
|
item.el.style.display = item.originalDisplay
|
||||||
|
}
|
||||||
|
})
|
||||||
|
hiddenElements.value = []
|
||||||
|
document.querySelector('.tags-action-btn').blur()
|
||||||
|
isFullscreen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropdownCommand(command) {
|
||||||
|
const tag = selectedDropdownTag.value
|
||||||
|
selectedTag.value = tag
|
||||||
|
switch (command) {
|
||||||
|
case 'refresh': refreshSelectedTag(tag); break
|
||||||
|
case 'fullscreen': toggleFullscreen(); break
|
||||||
|
case 'close': closeSelectedTag(tag); break
|
||||||
|
case 'closeOthers': closeOthersTags(); break
|
||||||
|
case 'closeLeft': closeLeftTags(); break
|
||||||
|
case 'closeRight': closeRightTags(); break
|
||||||
|
case 'closeAll': closeAllTags(tag); break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSelectedTag(view) {
|
||||||
|
proxy.$tab.refreshPage(view)
|
||||||
|
if (route.meta.link) {
|
||||||
|
useTagsViewStore().delIframeView(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSelectedTag(view) {
|
||||||
|
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
||||||
|
if (isActive(view)) {
|
||||||
|
toLastView(visitedViews, view)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRightTags() {
|
||||||
|
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
|
||||||
|
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLeftTags() {
|
||||||
|
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
|
||||||
|
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOthersTags() {
|
||||||
|
router.push(selectedTag.value).catch(() => { })
|
||||||
|
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||||
|
moveToCurrentTag()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllTags(view) {
|
||||||
|
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
||||||
|
if (affixTags.value.some(tag => tag.path === route.path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toLastView(visitedViews, view)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLastView(visitedViews, view) {
|
||||||
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
|
if (latestView) {
|
||||||
|
router.push(latestView.fullPath)
|
||||||
|
} else {
|
||||||
|
if (view && view.name === 'Dashboard') {
|
||||||
|
router.replace({ path: '/redirect' + view.fullPath })
|
||||||
|
} else {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu(tag, e) {
|
||||||
|
left.value = e.clientX
|
||||||
|
top.value = e.clientY
|
||||||
|
visible.value = true
|
||||||
|
selectedTag.value = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
closeMenu()
|
||||||
|
updateArrowState()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -394,8 +375,8 @@ $tags-bar-height: 34px;
|
|||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: $tags-bar-height;
|
height: $tags-bar-height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: var(--tags-bg, #fff);
|
||||||
border-bottom: 1px solid #d8dce5;
|
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -405,7 +386,7 @@ $tags-bar-height: 34px;
|
|||||||
$btn-hover-bg: #f0f2f5;
|
$btn-hover-bg: #f0f2f5;
|
||||||
$btn-hover-color: #303133;
|
$btn-hover-color: #303133;
|
||||||
$btn-disabled-color: #c0c4cc;
|
$btn-disabled-color: #c0c4cc;
|
||||||
$divider: 1px solid #d8dce5;
|
$divider: 1px solid var(--tags-item-border, #d8dce5);
|
||||||
|
|
||||||
.tags-nav-btn {
|
.tags-nav-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -440,27 +421,33 @@ $tags-bar-height: 34px;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
border: 1px solid #d8dce5;
|
border: 1px solid var(--tags-item-border, #d8dce5);
|
||||||
color: #495060;
|
color: var(--tags-item-text, #495060);
|
||||||
background: #fff;
|
background: var(--tags-item-bg, #fff);
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding-top: 2px !important;
|
||||||
|
|
||||||
&:first-of-type { margin-left: 6px; }
|
&:first-of-type { margin-left: 6px; }
|
||||||
&:last-of-type { margin-right: 15px; }
|
&:last-of-type { margin-right: 15px; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
|
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
|
||||||
background-color: #42b983;
|
background-color: #42b983;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #42b983;
|
border-color: #42b983;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -469,7 +456,7 @@ $tags-bar-height: 34px;
|
|||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 2px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +495,7 @@ $tags-bar-height: 34px;
|
|||||||
|
|
||||||
.contextmenu {
|
.contextmenu {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #fff;
|
background: var(--el-bg-color-overlay, #fff);
|
||||||
z-index: 3000;
|
z-index: 3000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
@@ -516,22 +503,28 @@ $tags-bar-height: 34px;
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #333;
|
color: var(--tags-item-text, #333);
|
||||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||||
|
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 7px 16px;
|
padding: 7px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #eee;
|
background: var(--tags-item-hover, #eee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.tags-view-container--chrome {
|
&.tags-view-container--chrome {
|
||||||
--chrome-strip-bg: #ffffff;
|
--chrome-strip-bg: #ffffff;
|
||||||
--chrome-strip-border: #e4e7ed;
|
--chrome-strip-border: var(--el-border-color-lighter, #e4e7ed);
|
||||||
--chrome-tab-text: #606266;
|
--chrome-tab-active-bg: var(--el-color-primary-light-9);
|
||||||
|
--chrome-tab-text: var(--el-text-color-regular, #606266);
|
||||||
|
--chrome-tab-text-active: var(--el-color-primary);
|
||||||
|
--chrome-wing-r: 10px;
|
||||||
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
background: var(--chrome-strip-bg);
|
background: var(--chrome-strip-bg);
|
||||||
@@ -566,7 +559,7 @@ $tags-bar-height: 34px;
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: var(--chrome-tab-text) !important;
|
color: var(--chrome-tab-text);
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
transition: background 0.12s ease, color 0.12s ease, border-radius 0.12s ease;
|
transition: background 0.12s ease, color 0.12s ease, border-radius 0.12s ease;
|
||||||
@@ -598,18 +591,23 @@ $tags-bar-height: 34px;
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-of-type { margin-left: 6px; }
|
&:first-of-type {
|
||||||
&:last-of-type { margin-right: 10px; }
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.active) + .tags-view-item:not(.active) {
|
&:not(.active) + .tags-view-item:not(.active) {
|
||||||
border-left: 1px solid #e4e7ed;
|
border-left: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||||
padding-left: 11px;
|
padding-left: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.active) {
|
&:hover:not(.active) {
|
||||||
background: #f5f7fa !important;
|
background: var(--el-fill-color-light, #f5f7fa) !important;
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
color: #303133 !important;
|
color: var(--el-text-color-primary, #303133);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@@ -631,12 +629,6 @@ $tags-bar-height: 34px;
|
|||||||
box-shadow: calc(var(--chrome-wing-r) * -0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
|
box-shadow: calc(var(--chrome-wing-r) * -0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-icon-close {
|
|
||||||
margin-left: 3px;
|
|
||||||
&:before {
|
|
||||||
vertical-align: -2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,74 +637,34 @@ $tags-bar-height: 34px;
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
.el-scrollbar {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-scrollbar__wrap {
|
|
||||||
height: 34px !important;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-view-container:hover & {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-scrollbar__bar {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
|
|
||||||
.tags-view-container:hover & {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
.el-icon-close {
|
.tags-close-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
vertical-align: 2px;
|
margin-left: 4px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
text-align: center;
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
cursor: pointer;
|
||||||
transform-origin: 100% 50%;
|
|
||||||
&:before {
|
.el-icon-close {
|
||||||
transform: scale(.6);
|
width: 1em;
|
||||||
display: inline-block;
|
height: 1em;
|
||||||
vertical-align: -3px;
|
vertical-align: 0;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #b4bccc;
|
background-color: var(--tags-close-hover, #b4bccc);
|
||||||
color: #fff;
|
|
||||||
|
.el-icon-close {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-menu class="topbar-menu" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
|
<el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
|
||||||
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
|
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
|
||||||
|
|
||||||
<el-submenu index="more" class="el-submenu__hide-arrow" v-if="moreRoutes.length > 0">
|
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
|
||||||
<template slot="title">更多菜单</template>
|
<template #title>
|
||||||
|
<span>更多菜单</span>
|
||||||
|
</template>
|
||||||
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
|
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
|
||||||
</el-submenu>
|
</el-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import SidebarItem from '../Sidebar/SidebarItem'
|
import SidebarItem from '../Sidebar/SidebarItem'
|
||||||
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
components: { SidebarItem },
|
const appStore = useAppStore()
|
||||||
data() {
|
const settingsStore = useSettingsStore()
|
||||||
return {
|
const permissionStore = usePermissionStore()
|
||||||
// 顶部栏初始数
|
|
||||||
visibleNumber: 5
|
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
|
||||||
}
|
const theme = computed(() => settingsStore.theme)
|
||||||
},
|
const device = computed(() => appStore.device)
|
||||||
computed: {
|
const activeMenu = computed(() => {
|
||||||
theme() {
|
const { meta, path } = route
|
||||||
return this.$store.state.settings.theme
|
if (meta.activeMenu) {
|
||||||
},
|
return meta.activeMenu
|
||||||
topMenus() {
|
|
||||||
return this.$store.state.permission.sidebarRouters.filter((f) => !f.hidden).slice(0, this.visibleNumber)
|
|
||||||
},
|
|
||||||
moreRoutes() {
|
|
||||||
const sidebarRouters = this.$store.state.permission.sidebarRouters;
|
|
||||||
return sidebarRouters.filter((f) => !f.hidden).slice(this.visibleNumber, sidebarRouters.length - this.visibleNumber)
|
|
||||||
},
|
|
||||||
// 默认激活的菜单
|
|
||||||
activeMenu() {
|
|
||||||
const { meta, path } = this.$route
|
|
||||||
if (meta.activeMenu) {
|
|
||||||
return meta.activeMenu
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
window.addEventListener('resize', this.setVisibleNumber)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('resize', this.setVisibleNumber)
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setVisibleNumber()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// 根据宽度计算设置显示栏数
|
|
||||||
setVisibleNumber() {
|
|
||||||
const width = document.body.getBoundingClientRect().width / 3
|
|
||||||
this.visibleNumber = parseInt(width / 85)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleNumber = ref(5)
|
||||||
|
const topMenus = computed(() => {
|
||||||
|
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
|
||||||
|
})
|
||||||
|
const moreRoutes = computed(() => {
|
||||||
|
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value)
|
||||||
|
})
|
||||||
|
function setVisibleNumber() {
|
||||||
|
const width = document.body.getBoundingClientRect().width / 3
|
||||||
|
visibleNumber.value = Math.max(1, parseInt(width / 85))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setVisibleNumber()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -65,34 +63,37 @@ export default {
|
|||||||
padding: 0 10px !important;
|
padding: 0 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu--horizontal .el-menu--popup .el-menu-item:hover {
|
.topbar-menu.el-menu--horizontal > .el-menu-item {
|
||||||
background-color: #f5f7fa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* submenu item */
|
|
||||||
.topbar-menu.el-menu--horizontal > .el-submenu .el-submenu__title {
|
|
||||||
float: left;
|
float: left;
|
||||||
height: 47px !important;
|
height: 50px !important;
|
||||||
line-height: 50px !important;
|
line-height: 50px !important;
|
||||||
color: #303133;
|
color: #303133 !important;
|
||||||
margin: 0 15px !important;
|
padding: 0 5px !important;
|
||||||
|
margin: 0 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* topbar more arrow */
|
.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
|
||||||
.topbar-menu .el-submenu .el-submenu__icon-arrow {
|
color: v-bind(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sub-menu item */
|
||||||
|
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
||||||
|
float: left;
|
||||||
|
line-height: 50px !important;
|
||||||
|
color: #303133 !important;
|
||||||
|
margin: 0 15px -3px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* topbar more arrow */
|
||||||
|
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
|
||||||
position: static;
|
position: static;
|
||||||
vertical-align: middle;
|
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* menu__title el-menu-item */
|
/* menu__title el-menu-item */
|
||||||
.topbar-menu.el-menu--horizontal .el-submenu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
|
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
|
||||||
height: 55px;
|
height: 60px;
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu--horizontal .el-menu .el-menu-item, .el-menu--horizontal .el-menu .el-submenu__title{
|
|
||||||
color: #303133;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
|
:ellipsis="false"
|
||||||
>
|
>
|
||||||
<template v-for="(item, index) in topMenus">
|
<template v-for="(item, index) in topMenus">
|
||||||
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
|
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
|
||||||
@@ -14,159 +15,167 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 顶部菜单超出数量折叠 -->
|
<!-- 顶部菜单超出数量折叠 -->
|
||||||
<el-submenu :style="{'--theme': theme}" index="more" :key="visibleNumber" v-if="topMenus.length > visibleNumber">
|
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
|
||||||
<template slot="title">更多菜单</template>
|
<template #title>更多菜单</template>
|
||||||
<template v-for="(item, index) in topMenus">
|
<template v-for="(item, index) in topMenus">
|
||||||
<el-menu-item
|
<el-menu-item
|
||||||
:index="item.path"
|
:index="item.path"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-if="index >= visibleNumber">
|
v-if="index >= visibleNumber">
|
||||||
<svg-icon
|
<svg-icon
|
||||||
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
|
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
|
||||||
:icon-class="item.meta.icon"/>
|
:icon-class="item.meta.icon"/>
|
||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</el-submenu>
|
</el-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { constantRoutes } from "@/router"
|
import { constantRoutes } from "@/router"
|
||||||
import { isHttp } from "@/utils/validate"
|
import { isHttp } from '@/utils/validate'
|
||||||
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
|
// 顶部栏初始数
|
||||||
|
const visibleNumber = ref(null)
|
||||||
|
// 当前激活菜单的 index
|
||||||
|
const currentIndex = ref(null)
|
||||||
// 隐藏侧边栏路由
|
// 隐藏侧边栏路由
|
||||||
const hideList = ['/index', '/user/profile']
|
const hideList = ['/index', '/user/profile']
|
||||||
|
|
||||||
export default {
|
const appStore = useAppStore()
|
||||||
data() {
|
const settingsStore = useSettingsStore()
|
||||||
return {
|
const permissionStore = usePermissionStore()
|
||||||
// 顶部栏初始数
|
const route = useRoute()
|
||||||
visibleNumber: 5,
|
const router = useRouter()
|
||||||
// 当前激活菜单的 index
|
|
||||||
currentIndex: undefined
|
// 主题颜色
|
||||||
}
|
const theme = computed(() => settingsStore.theme)
|
||||||
},
|
// 所有的路由信息
|
||||||
computed: {
|
const routers = computed(() => permissionStore.topbarRouters)
|
||||||
theme() {
|
|
||||||
return this.$store.state.settings.theme
|
// 顶部显示菜单
|
||||||
},
|
const topMenus = computed(() => {
|
||||||
// 顶部显示菜单
|
let topMenus = []
|
||||||
topMenus() {
|
routers.value.map((menu) => {
|
||||||
let topMenus = []
|
if (menu.hidden !== true) {
|
||||||
this.routers.map((menu) => {
|
// 兼容顶部栏一级菜单内部跳转
|
||||||
if (menu.hidden !== true) {
|
if (menu.path === '/' && menu.children) {
|
||||||
// 兼容顶部栏一级菜单内部跳转
|
topMenus.push(menu.children[0])
|
||||||
if (menu.path === '/' && menu.children) {
|
} else {
|
||||||
topMenus.push(menu.children[0])
|
topMenus.push(menu)
|
||||||
} else {
|
|
||||||
topMenus.push(menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return topMenus
|
|
||||||
},
|
|
||||||
// 所有的路由信息
|
|
||||||
routers() {
|
|
||||||
return this.$store.state.permission.topbarRouters
|
|
||||||
},
|
|
||||||
// 设置子路由
|
|
||||||
childrenMenus() {
|
|
||||||
var childrenMenus = []
|
|
||||||
this.routers.map((router) => {
|
|
||||||
for (var item in router.children) {
|
|
||||||
if (router.children[item].parentPath === undefined) {
|
|
||||||
if(router.path === "/") {
|
|
||||||
router.children[item].path = "/" + router.children[item].path
|
|
||||||
} else {
|
|
||||||
if(!isHttp(router.children[item].path)) {
|
|
||||||
router.children[item].path = router.path + "/" + router.children[item].path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
router.children[item].parentPath = router.path
|
|
||||||
}
|
|
||||||
childrenMenus.push(router.children[item])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return constantRoutes.concat(childrenMenus)
|
|
||||||
},
|
|
||||||
// 默认激活的菜单
|
|
||||||
activeMenu() {
|
|
||||||
const path = this.$route.path
|
|
||||||
let activePath = path
|
|
||||||
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
|
|
||||||
const tmpPath = path.substring(1, path.length)
|
|
||||||
if (!this.$route.meta.link) {
|
|
||||||
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
|
|
||||||
this.$store.dispatch('app/toggleSideBarHide', false)
|
|
||||||
}
|
|
||||||
} else if(!this.$route.children) {
|
|
||||||
activePath = path
|
|
||||||
this.$store.dispatch('app/toggleSideBarHide', true)
|
|
||||||
}
|
}
|
||||||
this.activeRoutes(activePath)
|
}
|
||||||
return activePath
|
})
|
||||||
},
|
return topMenus
|
||||||
},
|
})
|
||||||
beforeMount() {
|
|
||||||
window.addEventListener('resize', this.setVisibleNumber)
|
// 设置子路由
|
||||||
},
|
const childrenMenus = computed(() => {
|
||||||
beforeDestroy() {
|
let childrenMenus = []
|
||||||
window.removeEventListener('resize', this.setVisibleNumber)
|
routers.value.map((router) => {
|
||||||
},
|
for (let item in router.children) {
|
||||||
mounted() {
|
if (router.children[item].parentPath === undefined) {
|
||||||
this.setVisibleNumber()
|
if(router.path === "/") {
|
||||||
},
|
router.children[item].path = "/" + router.children[item].path
|
||||||
methods: {
|
|
||||||
// 根据宽度计算设置显示栏数
|
|
||||||
setVisibleNumber() {
|
|
||||||
const width = document.body.getBoundingClientRect().width / 3
|
|
||||||
this.visibleNumber = parseInt(width / 85)
|
|
||||||
},
|
|
||||||
// 菜单选择事件
|
|
||||||
handleSelect(key, keyPath) {
|
|
||||||
this.currentIndex = key
|
|
||||||
const route = this.routers.find(item => item.path === key)
|
|
||||||
if (isHttp(key)) {
|
|
||||||
// http(s):// 路径新窗口打开
|
|
||||||
window.open(key, "_blank")
|
|
||||||
} else if (!route || !route.children) {
|
|
||||||
// 没有子路由路径内部打开
|
|
||||||
const routeMenu = this.childrenMenus.find(item => item.path === key)
|
|
||||||
if (routeMenu && routeMenu.query) {
|
|
||||||
let query = JSON.parse(routeMenu.query)
|
|
||||||
this.$router.push({ path: key, query: query })
|
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({ path: key })
|
if(!isHttp(router.children[item].path)) {
|
||||||
}
|
router.children[item].path = router.path + "/" + router.children[item].path
|
||||||
this.$store.dispatch('app/toggleSideBarHide', true)
|
|
||||||
} else {
|
|
||||||
// 显示左侧联动菜单
|
|
||||||
this.activeRoutes(key)
|
|
||||||
this.$store.dispatch('app/toggleSideBarHide', false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 当前激活的路由
|
|
||||||
activeRoutes(key) {
|
|
||||||
var routes = []
|
|
||||||
if (this.childrenMenus && this.childrenMenus.length > 0) {
|
|
||||||
this.childrenMenus.map((item) => {
|
|
||||||
if (key == item.parentPath || (key == "index" && "" == item.path)) {
|
|
||||||
routes.push(item)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
router.children[item].parentPath = router.path
|
||||||
if(routes.length > 0) {
|
|
||||||
this.$store.commit("SET_SIDEBAR_ROUTERS", routes)
|
|
||||||
} else {
|
|
||||||
this.$store.dispatch('app/toggleSideBarHide', true)
|
|
||||||
}
|
}
|
||||||
|
childrenMenus.push(router.children[item])
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
return constantRoutes.concat(childrenMenus)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 默认激活的菜单
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
let activePath = path
|
||||||
|
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
|
||||||
|
const tmpPath = path.substring(1, path.length)
|
||||||
|
if (!route.meta.link) {
|
||||||
|
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
|
||||||
|
appStore.toggleSideBarHide(false)
|
||||||
|
}
|
||||||
|
} else if(!route.children) {
|
||||||
|
activePath = path
|
||||||
|
appStore.toggleSideBarHide(true)
|
||||||
|
}
|
||||||
|
activeRoutes(activePath)
|
||||||
|
return activePath
|
||||||
|
})
|
||||||
|
|
||||||
|
function setVisibleNumber() {
|
||||||
|
const width = document.body.getBoundingClientRect().width / 3
|
||||||
|
visibleNumber.value = Math.max(1, parseInt(width / 85))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(key, keyPath) {
|
||||||
|
currentIndex.value = key
|
||||||
|
const route = routers.value.find(item => item.path === key)
|
||||||
|
if (isHttp(key)) {
|
||||||
|
// http(s):// 路径新窗口打开
|
||||||
|
window.open(key, "_blank")
|
||||||
|
} else if (!route || !route.children) {
|
||||||
|
// 没有子路由路径内部打开
|
||||||
|
const routeMenu = childrenMenus.value.find(item => item.path === key)
|
||||||
|
if (routeMenu && routeMenu.query) {
|
||||||
|
let query = JSON.parse(routeMenu.query)
|
||||||
|
router.push({ path: key, query: query })
|
||||||
|
} else {
|
||||||
|
router.push({ path: key })
|
||||||
|
}
|
||||||
|
appStore.toggleSideBarHide(true)
|
||||||
|
} else {
|
||||||
|
// 显示左侧联动菜单
|
||||||
|
activeRoutes(key)
|
||||||
|
appStore.toggleSideBarHide(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activeRoutes(key) {
|
||||||
|
let routes = []
|
||||||
|
if (childrenMenus.value && childrenMenus.value.length > 0) {
|
||||||
|
childrenMenus.value.map((item) => {
|
||||||
|
if (key == item.parentPath || (key == "index" && "" == item.path)) {
|
||||||
|
routes.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(routes.length > 0) {
|
||||||
|
permissionStore.setSidebarRouters(routes)
|
||||||
|
} else {
|
||||||
|
appStore.toggleSideBarHide(true)
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setVisibleNumber()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.topmenu-container.el-menu--horizontal {
|
||||||
|
height: 50px !important;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.topmenu-container.el-menu--horizontal > .el-menu-item {
|
.topmenu-container.el-menu--horizontal > .el-menu-item {
|
||||||
float: left;
|
float: left;
|
||||||
height: 50px !important;
|
height: 50px !important;
|
||||||
@@ -176,13 +185,13 @@ export default {
|
|||||||
margin: 0 10px !important;
|
margin: 0 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
|
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
|
||||||
border-bottom: 2px solid #{'var(--theme)'} !important;
|
border-bottom: 2px solid #{'var(--theme)'} !important;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* submenu item */
|
/* sub-menu item */
|
||||||
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
|
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
||||||
float: left;
|
float: left;
|
||||||
height: 50px !important;
|
height: 50px !important;
|
||||||
line-height: 50px !important;
|
line-height: 50px !important;
|
||||||
@@ -190,4 +199,22 @@ export default {
|
|||||||
padding: 0 5px !important;
|
padding: 0 5px !important;
|
||||||
margin: 0 10px !important;
|
margin: 0 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 背景色隐藏 */
|
||||||
|
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标右间距 */
|
||||||
|
.topmenu-container .svg-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* topmenu more arrow */
|
||||||
|
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
|
||||||
|
position: static;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export { default as AppMain } from './AppMain'
|
export { default as AppMain } from './AppMain'
|
||||||
export { default as Navbar } from './Navbar'
|
export { default as Navbar } from './Navbar'
|
||||||
export { default as Settings } from './Settings'
|
export { default as Settings } from './Settings'
|
||||||
export { default as Sidebar } from './Sidebar/index.vue'
|
|
||||||
export { default as TagsView } from './TagsView/index.vue'
|
export { default as TagsView } from './TagsView/index.vue'
|
||||||
|
|||||||
@@ -1,115 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33'}">
|
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33' }">
|
||||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||||
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
|
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||||
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
|
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||||
<div :class="{'fixed-header':fixedHeader}">
|
<div :class="{ 'fixed-header': fixedHeader }">
|
||||||
<navbar @setLayout="setLayout"/>
|
<navbar @setLayout="setLayout" />
|
||||||
<tags-view v-if="needTagsView"/>
|
<tags-view v-if="needTagsView" />
|
||||||
</div>
|
</div>
|
||||||
<app-main/>
|
<app-main />
|
||||||
<settings ref="settingRef"/>
|
<settings ref="settingRef" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import ResizeMixin from './mixin/ResizeHandler'
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
import { mapState } from 'vuex'
|
import { AppMain, Navbar, Settings, TagsView } from './components'
|
||||||
import variables from '@/assets/styles/variables.scss'
|
import useAppStore from '@/store/modules/app'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
|
||||||
export default {
|
const settingsStore = useSettingsStore()
|
||||||
name: 'Layout',
|
const theme = computed(() => settingsStore.theme)
|
||||||
components: {
|
const sidebar = computed(() => useAppStore().sidebar)
|
||||||
AppMain,
|
const device = computed(() => useAppStore().device)
|
||||||
Navbar,
|
const needTagsView = computed(() => settingsStore.tagsView)
|
||||||
Settings,
|
const fixedHeader = computed(() => settingsStore.fixedHeader)
|
||||||
Sidebar,
|
|
||||||
TagsView
|
const classObj = computed(() => ({
|
||||||
},
|
hideSidebar: !sidebar.value.opened,
|
||||||
mixins: [ResizeMixin],
|
openSidebar: sidebar.value.opened,
|
||||||
computed: {
|
withoutAnimation: sidebar.value.withoutAnimation,
|
||||||
...mapState({
|
mobile: device.value === 'mobile'
|
||||||
theme: state => state.settings.theme,
|
}))
|
||||||
sideTheme: state => state.settings.sideTheme,
|
|
||||||
sidebar: state => state.app.sidebar,
|
const { width, height } = useWindowSize()
|
||||||
device: state => state.app.device,
|
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||||
needTagsView: state => state.settings.tagsView,
|
|
||||||
fixedHeader: state => state.settings.fixedHeader
|
watch(() => device.value, () => {
|
||||||
}),
|
if (device.value === 'mobile' && sidebar.value.opened) {
|
||||||
classObj() {
|
useAppStore().closeSideBar({ withoutAnimation: false })
|
||||||
return {
|
|
||||||
hideSidebar: !this.sidebar.opened,
|
|
||||||
openSidebar: this.sidebar.opened,
|
|
||||||
withoutAnimation: this.sidebar.withoutAnimation,
|
|
||||||
mobile: this.device === 'mobile'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
variables() {
|
|
||||||
return variables
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClickOutside() {
|
|
||||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
|
||||||
},
|
|
||||||
setLayout() {
|
|
||||||
this.$refs.settingRef.openSetting()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (width.value - 1 < WIDTH) {
|
||||||
|
useAppStore().toggleDevice('mobile')
|
||||||
|
useAppStore().closeSideBar({ withoutAnimation: true })
|
||||||
|
} else {
|
||||||
|
useAppStore().toggleDevice('desktop')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClickOutside() {
|
||||||
|
useAppStore().closeSideBar({ withoutAnimation: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingRef = ref(null)
|
||||||
|
function setLayout() {
|
||||||
|
settingRef.value.openSetting()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "~@/assets/styles/mixin.scss";
|
@use "@/assets/styles/mixin.scss" as mix;
|
||||||
@import "~@/assets/styles/variables.scss";
|
@use "@/assets/styles/variables.module.scss" as vars;
|
||||||
|
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
@include clearfix;
|
@include mix.clearfix;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.mobile.openSidebar {
|
&.mobile.openSidebar {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-container:has(.fixed-header) {
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-bg {
|
|
||||||
background: #000;
|
|
||||||
opacity: 0.3;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed-header {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
|
||||||
z-index: 9;
|
|
||||||
width: calc(100% - #{$base-sidebar-width});
|
|
||||||
transition: width 0.28s;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hideSidebar .fixed-header {
|
.main-container:has(.fixed-header) {
|
||||||
width: calc(100% - 54px);
|
height: 100vh;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebarHide .fixed-header {
|
.drawer-bg {
|
||||||
width: 100%;
|
background: #000;
|
||||||
}
|
opacity: 0.3;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile .fixed-header {
|
.fixed-header {
|
||||||
width: 100%;
|
position: fixed;
|
||||||
}
|
top: 0;
|
||||||
</style>
|
right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: calc(100% - #{vars.$base-sidebar-width});
|
||||||
|
transition: width 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideSidebar .fixed-header {
|
||||||
|
width: calc(100% - 54px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHide .fixed-header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .fixed-header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import store from '@/store'
|
|
||||||
|
|
||||||
const { body } = document
|
|
||||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
|
||||||
|
|
||||||
export default {
|
|
||||||
watch: {
|
|
||||||
$route(route) {
|
|
||||||
if (this.device === 'mobile' && this.sidebar.opened) {
|
|
||||||
store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
window.addEventListener('resize', this.$_resizeHandler)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('resize', this.$_resizeHandler)
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const isMobile = this.$_isMobile()
|
|
||||||
if (isMobile) {
|
|
||||||
store.dispatch('app/toggleDevice', 'mobile')
|
|
||||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// use $_ for mixins properties
|
|
||||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
|
||||||
$_isMobile() {
|
|
||||||
const rect = body.getBoundingClientRect()
|
|
||||||
return rect.width - 1 < WIDTH
|
|
||||||
},
|
|
||||||
$_resizeHandler() {
|
|
||||||
if (!document.hidden) {
|
|
||||||
const isMobile = this.$_isMobile()
|
|
||||||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+49
-48
@@ -1,28 +1,38 @@
|
|||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
import Element from 'element-ui'
|
import ElementPlus from 'element-plus'
|
||||||
import './assets/styles/element-variables.scss'
|
import 'element-plus/dist/index.css'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
import locale from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
|
||||||
import '@/assets/styles/index.scss' // global css
|
import '@/assets/styles/index.scss' // global css
|
||||||
import '@/assets/styles/ruoyi.scss' // ruoyi css
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import directive from './directive' // directive
|
import directive from './directive' // directive
|
||||||
|
|
||||||
|
// 注册指令
|
||||||
import plugins from './plugins' // plugins
|
import plugins from './plugins' // plugins
|
||||||
import { download } from '@/utils/request'
|
import { download } from '@/utils/request'
|
||||||
|
|
||||||
import './assets/icons' // icon
|
// svg图标
|
||||||
|
import 'virtual:svg-icons-register'
|
||||||
|
import SvgIcon from '@/components/SvgIcon'
|
||||||
|
import elementIcons from '@/components/SvgIcon/svgicon'
|
||||||
|
|
||||||
import './permission' // permission control
|
import './permission' // permission control
|
||||||
import { getDicts } from "@/api/system/dict/data"
|
|
||||||
|
import { useDict } from '@/utils/dict'
|
||||||
import { getConfigKey } from "@/api/system/config"
|
import { getConfigKey } from "@/api/system/config"
|
||||||
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"
|
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
|
||||||
|
|
||||||
// 分页组件
|
// 分页组件
|
||||||
import Pagination from "@/components/Pagination"
|
import Pagination from '@/components/Pagination'
|
||||||
// 自定义表格工具组件
|
// 自定义表格工具组件
|
||||||
import RightToolbar from "@/components/RightToolbar"
|
import RightToolbar from '@/components/RightToolbar'
|
||||||
// 富文本组件
|
// 富文本组件
|
||||||
import Editor from "@/components/Editor"
|
import Editor from "@/components/Editor"
|
||||||
// 文件上传组件
|
// 文件上传组件
|
||||||
@@ -33,51 +43,42 @@ import ImageUpload from "@/components/ImageUpload"
|
|||||||
import ImagePreview from "@/components/ImagePreview"
|
import ImagePreview from "@/components/ImagePreview"
|
||||||
// 字典标签组件
|
// 字典标签组件
|
||||||
import DictTag from '@/components/DictTag'
|
import DictTag from '@/components/DictTag'
|
||||||
// 字典数据组件
|
|
||||||
import DictData from '@/components/DictData'
|
const app = createApp(App)
|
||||||
|
|
||||||
// 全局方法挂载
|
// 全局方法挂载
|
||||||
Vue.prototype.getDicts = getDicts
|
app.config.globalProperties.useDict = useDict
|
||||||
Vue.prototype.getConfigKey = getConfigKey
|
app.config.globalProperties.download = download
|
||||||
Vue.prototype.parseTime = parseTime
|
app.config.globalProperties.parseTime = parseTime
|
||||||
Vue.prototype.resetForm = resetForm
|
app.config.globalProperties.resetForm = resetForm
|
||||||
Vue.prototype.addDateRange = addDateRange
|
app.config.globalProperties.handleTree = handleTree
|
||||||
Vue.prototype.selectDictLabel = selectDictLabel
|
app.config.globalProperties.addDateRange = addDateRange
|
||||||
Vue.prototype.selectDictLabels = selectDictLabels
|
app.config.globalProperties.getConfigKey = getConfigKey
|
||||||
Vue.prototype.download = download
|
app.config.globalProperties.selectDictLabel = selectDictLabel
|
||||||
Vue.prototype.handleTree = handleTree
|
app.config.globalProperties.selectDictLabels = selectDictLabels
|
||||||
|
|
||||||
// 全局组件挂载
|
// 全局组件挂载
|
||||||
Vue.component('DictTag', DictTag)
|
app.component('DictTag', DictTag)
|
||||||
Vue.component('Pagination', Pagination)
|
app.component('Pagination', Pagination)
|
||||||
Vue.component('RightToolbar', RightToolbar)
|
app.component('FileUpload', FileUpload)
|
||||||
Vue.component('Editor', Editor)
|
app.component('ImageUpload', ImageUpload)
|
||||||
Vue.component('FileUpload', FileUpload)
|
app.component('ImagePreview', ImagePreview)
|
||||||
Vue.component('ImageUpload', ImageUpload)
|
app.component('RightToolbar', RightToolbar)
|
||||||
Vue.component('ImagePreview', ImagePreview)
|
app.component('Editor', Editor)
|
||||||
|
|
||||||
Vue.use(directive)
|
app.use(router)
|
||||||
Vue.use(plugins)
|
app.use(store)
|
||||||
DictData.install()
|
app.use(plugins)
|
||||||
|
app.use(elementIcons)
|
||||||
|
app.component('svg-icon', SvgIcon)
|
||||||
|
|
||||||
/**
|
directive(app)
|
||||||
* If you don't want to use mock-server
|
|
||||||
* you want to use MockJs for mock api
|
|
||||||
* you can execute: mockXHR()
|
|
||||||
*
|
|
||||||
* Currently MockJs will be used in the production environment,
|
|
||||||
* please remove it before going online! ! !
|
|
||||||
*/
|
|
||||||
|
|
||||||
Vue.use(Element, {
|
// 使用element-plus 并且设置全局的大小
|
||||||
size: Cookies.get('size') || 'medium' // set element-ui default size
|
app.use(ElementPlus, {
|
||||||
|
locale: locale,
|
||||||
|
// 支持 large、default、small
|
||||||
|
size: Cookies.get('size') || 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
app.mount('#app')
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
render: h => h(App)
|
|
||||||
})
|
|
||||||
|
|||||||
+43
-37
@@ -1,11 +1,14 @@
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import store from './store'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Message } from 'element-ui'
|
|
||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
import { getToken } from '@/utils/auth'
|
import { getToken } from '@/utils/auth'
|
||||||
import { isPathMatch } from '@/utils/validate'
|
import { isHttp, isPathMatch } from '@/utils/validate'
|
||||||
import { isRelogin } from '@/utils/request'
|
import { isRelogin } from '@/utils/request'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
import useLockStore from '@/store/modules/lock'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import usePermissionStore from '@/store/modules/permission'
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false })
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
@@ -15,53 +18,56 @@ const isWhiteList = (path) => {
|
|||||||
return whiteList.some(pattern => isPathMatch(pattern, path))
|
return whiteList.some(pattern => isPathMatch(pattern, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach(async (to, from) => {
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
if (getToken()) {
|
if (getToken()) {
|
||||||
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
to.meta.title && useSettingsStore().setTitle(to.meta.title)
|
||||||
const isLock = store.getters.isLock
|
const isLock = useLockStore().isLock
|
||||||
/* has token*/
|
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
next({ path: '/' })
|
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
} else if (isWhiteList(to.path)) {
|
return { path: '/' }
|
||||||
next()
|
}
|
||||||
} else if (isLock && to.path !== '/lock') {
|
if (isWhiteList(to.path)) {
|
||||||
next({ path: '/lock' })
|
return true
|
||||||
|
}
|
||||||
|
if (isLock && to.path !== '/lock') {
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
} else if (!isLock && to.path === '/lock') {
|
return { path: '/lock' }
|
||||||
next({ path: '/' })
|
}
|
||||||
|
if (!isLock && to.path === '/lock') {
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
} else {
|
return { path: '/' }
|
||||||
if (store.getters.roles.length === 0) {
|
}
|
||||||
isRelogin.show = true
|
if (useUserStore().roles.length === 0) {
|
||||||
// 判断当前用户是否已拉取完user_info信息
|
isRelogin.show = true
|
||||||
store.dispatch('GetInfo').then(() => {
|
try {
|
||||||
isRelogin.show = false
|
// 拉取user_info信息
|
||||||
store.dispatch('GenerateRoutes').then(accessRoutes => {
|
await useUserStore().getInfo()
|
||||||
// 根据roles权限生成可访问的路由表
|
isRelogin.show = false
|
||||||
router.addRoutes(accessRoutes) // 动态添加可访问路由表
|
// 根据roles权限生成可访问的路由
|
||||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
|
const accessRoutes = await usePermissionStore().generateRoutes()
|
||||||
})
|
accessRoutes.forEach(route => {
|
||||||
}).catch(err => {
|
if (!isHttp(route.path)) {
|
||||||
store.dispatch('LogOut').then(() => {
|
router.addRoute(route)
|
||||||
Message.error(err)
|
}
|
||||||
next({ path: '/' })
|
})
|
||||||
})
|
// 重新导航到目标路由,确保动态路由已注册
|
||||||
})
|
return { ...to, replace: true }
|
||||||
} else {
|
} catch (err) {
|
||||||
next()
|
await useUserStore().logOut()
|
||||||
|
ElMessage.error(err)
|
||||||
|
return { path: '/' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
// 没有token
|
// 没有token
|
||||||
if (isWhiteList(to.path)) {
|
if (isWhiteList(to.path)) {
|
||||||
// 在免登录白名单,直接进入
|
// 在免登录白名单,直接进入
|
||||||
next()
|
return true
|
||||||
} else {
|
|
||||||
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
|
|
||||||
NProgress.done()
|
|
||||||
}
|
}
|
||||||
|
NProgress.done()
|
||||||
|
return `/login?redirect=${to.fullPath}` // 否则全部重定向到登录页
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user