This commit is contained in:
2026-04-24 10:37:05 +08:00
parent 1a6be585ff
commit 554a91f78e
203 changed files with 16148 additions and 18160 deletions
-22
View File
@@ -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
+3 -6
View File
@@ -1,11 +1,8 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 若依管理系统
# 开发环境配置
ENV = 'development'
VITE_APP_ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
VITE_APP_BASE_API = '/dev-api'
+6 -3
View File
@@ -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
+7 -8
View File
@@ -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'
# 测试环境配置
ENV = 'staging'
# 若依管理系统/测试环境
VUE_APP_BASE_API = '/stage-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
+20
View File
@@ -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
View File
@@ -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)&nbsp;&nbsp;
# 版本对比
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
# 克隆项目
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
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com
yarn --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
## 在线体验
# 构建生产环境
npm run build:prod
```
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址: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群: [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/已满-174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [![加入QQ群](https://img.shields.io/badge/127358632-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。
-13
View File
@@ -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']
}
}
}
+1 -1
View File
@@ -7,6 +7,6 @@ echo.
cd %~dp0
cd ..
npm run build:prod
yarn build:prod
pause
+1 -1
View File
@@ -7,6 +7,6 @@ echo.
cd %~dp0
cd ..
npm install --registry=https://registry.npmmirror.com
yarn --registry=https://registry.npmmirror.com
pause
+2 -2
View File
@@ -1,12 +1,12 @@
@echo off
echo.
echo [信息] 使用 Vue CLI 命令运行 Web 工程。
echo [信息] 使用 Vite 命令运行 Web 工程。
echo.
%~d0
cd %~dp0
cd ..
npm run dev
yarn dev
pause
-35
View File
@@ -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}`)
}
+236
View File
@@ -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
View File
@@ -4,68 +4,50 @@
"description": "若依管理系统",
"author": "若依",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview"
"dev": "vite",
"build:prod": "vite build",
"build:stage": "vite build --mode staging",
"preview": "vite preview"
},
"keywords": [
"vue",
"admin",
"dashboard",
"element-ui",
"boilerplate",
"admin-template",
"management-system"
],
"repository": {
"type": "git",
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
},
"dependencies": {
"@riophae/vue-treeselect": "0.4.0",
"axios": "0.30.3",
"clipboard": "2.0.8",
"core-js": "3.37.1",
"echarts": "5.4.0",
"element-ui": "2.15.14",
"@element-plus/icons-vue": "2.3.2",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "14.1.0",
"axios": "1.13.2",
"clipboard": "2.0.11",
"echarts": "5.6.0",
"element-plus": "2.13.1",
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1",
"fuse.js": "7.1.0",
"js-beautify": "1.15.4",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"nprogress": "0.2.0",
"quill": "2.0.2",
"screenfull": "5.0.2",
"sortablejs": "1.10.2",
"vue": "2.6.12",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-router": "3.4.9",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
"pinia": "3.0.4",
"vue": "3.5.26",
"vue-cropper": "1.1.1",
"vue-router": "4.6.4",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-service": "4.4.6",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "4.1.0",
"compression-webpack-plugin": "6.1.2",
"connect": "3.6.6",
"sass": "1.32.13",
"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"
"@vitejs/plugin-vue": "5.2.4",
"sass-embedded": "1.97.2",
"unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "6.4.1",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
"overrides": {
"quill": "2.0.2"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
"resolutions": {
"quill": "2.0.2"
}
}
-208
View File
@@ -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>
-2
View File
@@ -1,2 +0,0 @@
User-agent: *
Disallow: /
File diff suppressed because one or more lines are too long
+9 -14
View File
@@ -1,20 +1,15 @@
<template>
<div id="app">
<router-view />
<theme-picker />
</div>
</template>
<script>
import ThemePicker from "@/components/ThemePicker"
<script setup>
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
export default {
name: "App",
components: { ThemePicker }
}
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(useSettingsStore().theme)
})
})
</script>
<style scoped>
#app .theme-picker {
display: none;
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi"
import { parseStrEmpty } from "@/utils/ruoyi";
// 查询用户列表
export function listUser(query) {
-9
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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

+1
View File
@@ -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

-22
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
@import './variables.scss';
@use './variables.module.scss' as *;
@mixin colorBtn($color) {
background: $color;
@@ -90,3 +90,7 @@
.el-submenu__icon-arrow {
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;
}
+7 -6
View File
@@ -1,12 +1,13 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
@use './mixin.scss';
@use './transition.scss';
@use './element-ui.scss';
@use './sidebar.scss';
@use './btn.scss';
@use './ruoyi.scss';
body {
height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
+72 -62
View File
@@ -1,64 +1,51 @@
/**
* 通用css样式布局处理
* Copyright (c) 2019 ruoyi
*/
* 通用css样式布局处理
* Copyright (c) 2019 ruoyi
*/
/** 基础通用 **/
.pt5 {
padding-top: 5px;
}
.pr5 {
padding-right: 5px;
}
.pb5 {
padding-bottom: 5px;
}
.mt5 {
margin-top: 5px;
}
.mr5 {
margin-right: 5px;
}
.mb5 {
margin-bottom: 5px;
}
.mb8 {
margin-bottom: 8px;
}
.ml5 {
margin-left: 5px;
}
.mt10 {
margin-top: 10px;
}
.mr10 {
margin-right: 10px;
}
.mb10 {
margin-bottom: 10px;
}
.ml10 {
margin-left: 10px;
}
.mt20 {
margin-top: 20px;
}
.mr20 {
margin-right: 20px;
}
.mb20 {
margin-bottom: 20px;
}
@@ -73,19 +60,22 @@
color: inherit;
}
.el-message-box__status + .el-message-box__message{
word-break: break-word;
.el-form--inline {
.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) {
margin-top: 6vh !important;
}
.el-dialog__body {
padding: 8px 20px !important;
}
.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body {
.el-dialog.scrollbar .el-dialog__body {
overflow: auto;
overflow-x: hidden;
max-height: 70vh;
@@ -96,13 +86,12 @@
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
th {
word-break: break-word;
background-color: #f8f8f9;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px;
height: 40px !important;
font-size: 13px;
}
}
.el-table__body-wrapper {
.el-button [class*="el-icon-"] + span {
margin-left: 1px;
@@ -112,11 +101,11 @@
/** 表单布局 **/
.form-header {
font-size: 15px;
color: #6379bb;
border-bottom: 1px solid #ddd;
margin: 8px 10px 25px 10px;
padding-bottom: 5px
font-size:15px;
color:#6379bb;
border-bottom:1px solid #ddd;
margin:8px 10px 25px 10px;
padding-bottom:5px
}
/** 表格布局 **/
@@ -124,19 +113,52 @@
display: flex;
justify-content: flex-end;
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 {
margin-top: 5px;
border: 1px solid #e5e6e7;
background: #FFFFFF none;
border-radius: 4px;
border: 1px solid var(--el-border-color-light, #e5e6e7);
background: var(--el-bg-color, #FFFFFF) none;
border-radius:4px;
width: 100%;
}
.el-table .fixed-width .el-button--small {
padding-left: 0;
padding-right: 0;
width: inherit;
}
/* horizontal el menu */
.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;
}
@@ -144,25 +166,11 @@
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;
margin-left: 5px;
color: #409EFF;
margin-left: 10px;
}
.el-table .el-dropdown, .el-icon-arrow-down {
@@ -199,12 +207,12 @@
}
.el-card__header {
padding: 14px 15px 7px;
padding: 14px 15px 7px !important;
min-height: 40px;
}
.el-card__body {
padding: 15px 20px 20px 20px;
padding: 15px 20px 20px 20px !important;
}
.card-box {
@@ -234,6 +242,9 @@
/** 详细卡片样式 */
.detail-drawer {
.el-drawer__body {
padding: 0;
}
.el-drawer__header {
margin-bottom: 6px;
padding: 8px 12px 6px;
@@ -457,10 +468,9 @@
}
.avatar-upload-preview {
position: relative;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: translate(50%, -50%);
width: 200px;
height: 200px;
border-radius: 50%;
@@ -472,13 +482,13 @@
.allowDrag { cursor: grab; }
.allowDrag:active { cursor: grabbing; }
.sortable-ghost {
.sortable-ghost{
opacity: .8;
color: #fff !important;
background: #42b983 !important;
color: #fff!important;
background: #42b983!important;
}
/* 表格右侧工具栏样式 */
.top-right-btn {
position: relative;
float: right;
margin-left: auto;
}
+52 -45
View File
@@ -1,9 +1,11 @@
@use './variables.module.scss' as vars;
#app {
.main-container {
height: 100%;
min-height: 100%;
transition: margin-left .28s;
margin-left: $base-sidebar-width;
margin-left: vars.$base-sidebar-width;
position: relative;
}
@@ -12,10 +14,8 @@
}
.sidebar-container {
-webkit-transition: width .28s;
transition: width 0.28s;
width: $base-sidebar-width !important;
background-color: $base-menu-background;
width: vars.$base-sidebar-width !important;
height: 100%;
position: fixed;
font-size: 0px;
@@ -70,7 +70,7 @@
width: 100% !important;
}
.el-menu-item, .el-submenu__title {
.el-menu-item, .menu-title {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
@@ -78,33 +78,37 @@
line-height: 44px !important;
}
.el-menu-item .el-menu-tooltip__trigger {
display: inline-block !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
.sub-menu-title-noDropdown,
.el-sub-menu__title {
&: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 {
color: $base-menu-color-active !important;
& .theme-dark .is-active > .el-sub-menu__title {
color: vars.$base-menu-color-active !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $base-sidebar-width !important;
& .nest-menu .el-sub-menu>.el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: vars.$base-sidebar-width !important;
&: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 .el-submenu .el-menu-item {
background-color: $base-sub-menu-background !important;
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: vars.$base-sub-menu-background;
&:hover {
background-color: $base-sub-menu-hover !important;
background-color: vars.$base-sub-menu-hover !important;
}
}
@@ -121,19 +125,18 @@
position: absolute;
inset: 0;
background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
border-right: 3px solid var(--current-color, #409eff);
pointer-events: none;
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;
}
.el-menu-item:not(.is-active),
.submenu-title-noDropdown,
.el-submenu__title {
.el-sub-menu__title {
position: relative;
&::before {
@@ -158,7 +161,7 @@
box-shadow: none;
.el-menu-item,
.el-submenu__title {
.el-sub-menu__title {
color: rgba(0, 0, 0, 0.65);
}
@@ -171,29 +174,28 @@
position: absolute;
inset: 0;
background-color: var(--current-color-light, #ecf5ff);
border-right: 3px solid var(--current-color, #409eff);
pointer-events: none;
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;
}
.el-menu-item:not(.is-active):hover,
.submenu-title-noDropdown:hover,
.el-submenu__title:hover {
background-color: #f5f7fa !important;
.el-sub-menu__title:hover {
background-color: #f5f7fa;
color: rgba(0, 0, 0, 0.85) !important;
}
.nest-menu .el-submenu > .el-submenu__title,
.el-submenu .el-menu-item {
background-color: #fafafa !important;
.nest-menu .el-sub-menu > .el-sub-menu__title,
.el-sub-menu .el-menu-item {
background-color: #fafafa;
&:hover {
background-color: #f0f5ff !important;
background-color: #f0f5ff;
}
}
}
@@ -208,8 +210,7 @@
margin-left: 54px;
}
.el-menu:not(.el-menu--horizontal) {
.submenu-title-noDropdown {
.sub-menu-title-noDropdown {
padding: 0 !important;
position: relative;
@@ -221,12 +222,11 @@
}
}
}
}
.el-submenu {
.el-sub-menu {
overflow: hidden;
&>.el-submenu__title {
&>.el-sub-menu__title {
padding: 0 !important;
.svg-icon {
@@ -237,8 +237,8 @@
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
.el-sub-menu {
&>.el-sub-menu__title {
&>span {
height: 0;
width: 0;
@@ -246,13 +246,20 @@
visibility: hidden;
display: inline-block;
}
&>i {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $base-sidebar-width !important;
.el-menu--collapse .el-menu .el-sub-menu {
min-width: vars.$base-sidebar-width !important;
}
// mobile responsive
@@ -263,14 +270,14 @@
.sidebar-container {
transition: transform .28s;
width: $base-sidebar-width !important;
width: vars.$base-sidebar-width !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
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 {
&:hover {
// you can use $subMenuHover
// you can use $sub-menuHover
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 {
max-height: 100vh;
overflow-y: auto;
+34 -3
View File
@@ -6,7 +6,7 @@
transition: opacity 0.28s;
}
.fade-enter,
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}
@@ -18,7 +18,7 @@
transition: all .5s;
}
.fade-transform-enter {
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
@@ -34,7 +34,7 @@
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
@@ -47,3 +47,34 @@
.breadcrumb-leave-active {
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
}
+37 -42
View File
@@ -9,49 +9,36 @@
</el-breadcrumb>
</template>
<script>
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
<script setup>
import usePermissionStore from '@/store/modules/permission'
const route = useRoute()
const router = useRouter()
const permissionStore = usePermissionStore()
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = []
const router = this.$route
const pathNum = this.findPathNum(router.path)
const pathNum = findPathNum(route.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = router.path.match(reg).map((item, index) => {
const pathList = route.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
this.getMatched(pathList, this.$store.getters.defaultRoutes, matched)
getMatched(pathList, permissionStore.defaultRoutes, matched)
} else {
matched = router.matched.filter(item => item.meta && item.meta.title)
matched = route.matched.filter((item) => item.meta && item.meta.title)
}
// 判断是否为首页
if (!this.isDashboard(matched[0])) {
if (!isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
findPathNum(str, char = "/") {
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
@@ -59,41 +46,49 @@ export default {
index = str.indexOf(char, index + 1)
}
return num
},
getMatched(pathList, routeList, matched) {
}
function 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)
getMatched(pathList, data.children, matched)
}
}
},
isDashboard(route) {
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
},
handleLink(item) {
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
router.push(redirect)
return
}
this.$router.push(path)
}
}
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>
<style lang="scss" scoped>
<style lang='scss' scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
.no-redirect {
color: #97a8be;
cursor: text;
+113 -100
View File
@@ -1,161 +1,174 @@
<template>
<el-form size="small">
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L W]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="2">
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" />
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" />
</el-radio>
</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='average02' :min="1" :max="31 - average01 || 1" /> 日执行一次
<el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
</el-radio>
</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-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="6">
<el-radio v-model='radioValue' :value="6">
本月最后一天
</el-radio>
</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-option v-for="item in 31" :key="item" :value="item">{{item}}</el-option>
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 31" :key="item" :label="item" :value="item" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
radioValue: 1,
workday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
name: 'crontab-day',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
('day rachange')
if (this.radioValue !== 2 && this.cron.week !== '?') {
this.$emit('update', 'week', '?', 'day')
check: {
type: Function,
default: () => {
}
switch (this.radioValue) {
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const workday = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 30)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 30)
average02.value = props.check(average02.value, 1, 31 - average01.value)
return average01.value + '/' + average02.value
})
const workdayTotal = computed(() => {
workday.value = props.check(workday.value, 1, 31)
return workday.value + 'W'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.day, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else if (value.indexOf("W") > -1) {
const indexArr = value.split("W")
workday.value = Number(indexArr[0])
radioValue.value = 5
} else if (value === "L") {
radioValue.value = 6
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 7
}
}
// 单选按钮值变化时
function onRadioChange() {
if (radioValue.value === 2 && props.cron.week === '?') {
emit('update', 'week', '*', 'day')
}
if (radioValue.value !== 2 && props.cron.week !== '?') {
emit('update', 'week', '?', 'day')
}
switch (radioValue.value) {
case 1:
this.$emit('update', 'day', '*')
emit('update', 'day', '*', 'day')
break
case 2:
this.$emit('update', 'day', '?')
emit('update', 'day', '?', 'day')
break
case 3:
this.$emit('update', 'day', this.cycleTotal)
emit('update', 'day', cycleTotal.value, 'day')
break
case 4:
this.$emit('update', 'day', this.averageTotal)
emit('update', 'day', averageTotal.value, 'day')
break
case 5:
this.$emit('update', 'day', this.workday + 'W')
emit('update', 'day', workdayTotal.value, 'day')
break
case 6:
this.$emit('update', 'day', 'L')
emit('update', 'day', 'L', 'day')
break
case 7:
this.$emit('update', 'day', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'day', checkboxString.value, 'day')
break
}
('day rachange end')
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'day', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'day', this.averageTotal)
}
},
// 最近工作日值变化时
workdayChange() {
if (this.radioValue == '5') {
this.$emit('update', 'day', this.workdayCheck + 'W')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '7') {
this.$emit('update', 'day', this.checkboxString)
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'workdayCheck': 'workdayChange',
'checkboxString': 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 1, 30)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 31, 31)
return cycle01 + '-' + cycle02
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 1, 30)
const average02 = this.checkNum(this.average02, 1, 31 - average01 || 0)
return average01 + '/' + average02
},
// 计算工作日格式
workdayCheck: function () {
const workday = this.checkNum(this.workday, 1, 31)
return workday
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join()
return str == '' ? '*' : str
}
}
}
</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>
+93 -80
View File
@@ -1,120 +1,133 @@
<template>
<el-form size="small">
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
小时允许的通配符[, - * /]
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" />
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" />
</el-radio>
</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='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
<el-input-number v-model='average01' :min="0" :max="22" /> 时开始
<el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
</el-radio>
</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-option v-for="item in 24" :key="item" :value="item-1">{{item-1}}</el-option>
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
name: 'crontab-hour',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'hour')
check: {
type: Function,
default: () => {
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'hour')
}
switch (this.radioValue) {
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 22)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 22)
average02.value = props.check(average02.value, 1, 23 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.hour, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (props.cron.min === '*') {
emit('update', 'min', '0', 'hour')
}
if (props.cron.second === '*') {
emit('update', 'second', '0', 'hour')
}
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
this.$emit('update', 'hour', '*')
emit('update', 'hour', '*', 'hour')
break
case 2:
this.$emit('update', 'hour', this.cycleTotal)
emit('update', 'hour', cycleTotal.value, 'hour')
break
case 3:
this.$emit('update', 'hour', this.averageTotal)
emit('update', 'hour', averageTotal.value, 'hour')
break
case 4:
this.$emit('update', 'hour', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'hour', checkboxString.value, 'hour')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'hour', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'hour', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'hour', this.checkboxString)
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 0, 22)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
return cycle01 + '-' + cycle02
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 0, 22)
const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
return average01 + '/' + average02
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join()
return str == '' ? '*' : str
}
}
}
</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>
+103 -220
View File
@@ -70,49 +70,61 @@
<p class="title">时间表达式</p>
<table>
<thead>
<th v-for="item of tabTitles" width="40" :key="item">{{item}}</th>
<tr>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span>{{crontabValueObj.second}}</span>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.min}}</span>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.hour}}</span>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.day}}</span>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.month}}</span>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.week}}</span>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span>{{crontabValueObj.year}}</span>
<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>
<span>{{crontabValueString}}</span>
<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">
<el-button size="small" type="primary" @click="submitFill">确定</el-button>
<el-button size="small" type="warning" @click="clearCron">重置</el-button>
<el-button size="small" @click="hidePopup">取消</el-button>
<el-button type="primary" @click="submitFill">确定</el-button>
<el-button type="warning" @click="clearCron">重置</el-button>
<el-button @click="hidePopup">取消</el-button>
</div>
</div>
</div>
</template>
<script>
<script setup>
import CrontabSecond from "./second.vue"
import CrontabMin from "./min.vue"
import CrontabHour from "./hour.vue"
@@ -121,14 +133,23 @@ import CrontabMonth from "./month.vue"
import CrontabWeek from "./week.vue"
import CrontabYear from "./year.vue"
import CrontabResult from "./result.vue"
export default {
data() {
return {
tabTitles: ["秒", "分钟", "小时", "日", "月", "周", "年"],
tabActive: 0,
myindex: 0,
crontabValueObj: {
const { proxy } = getCurrentInstance()
const emit = defineEmits(['hide', 'fill'])
const props = defineProps({
hideComponent: {
type: Array,
default: () => [],
},
expression: {
type: String,
default: ""
}
})
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
const tabActive = ref(0)
const hideComponent = ref([])
const expression = ref('')
const crontabValueObj = ref({
second: "*",
min: "*",
hour: "*",
@@ -136,20 +157,30 @@ export default {
month: "*",
week: "?",
year: "",
},
}
},
name: "vcrontab",
props: ["expression", "hideComponent"],
methods: {
shouldHide(key) {
if (this.hideComponent && this.hideComponent.includes(key)) return false
return true
},
resolveExp() {
})
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 (this.expression) {
let arr = this.expression.split(" ")
if (expression.value) {
const arr = expression.value.split(/\s+/)
if (arr.length >= 6) {
//6 位以上是合法表达式
let obj = {
@@ -159,140 +190,27 @@ export default {
day: arr[3],
month: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : "",
year: arr[6] ? arr[6] : ""
}
this.crontabValueObj = {
crontabValueObj.value = {
...obj,
}
for (let i in obj) {
if (obj[i]) this.changeRadio(i, obj[i])
}
}
} else {
// 没有传入的表达式 则还原
this.clearCron()
clearCron()
}
},
// tab切换值
tabCheck(index) {
this.tabActive = index
},
// 由子组件触发,更改表达式组成的字段值
updateCrontabValue(name, value, from) {
"updateCrontabValue", name, value, from
this.crontabValueObj[name] = value
if (from && from !== name) {
console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
this.changeRadio(name, value)
}
},
// 赋值到组件
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) {
}
// tab切换值
function tabCheck(index) {
tabActive.value = index
}
// 由子组件触发,更改表达式组成的字段值
function updateCrontabValue(name, value, from) {
crontabValueObj.value[name] = value
}
// 表单选项的子组件校验数字格式(通过-props传递)
function checkNumber(value, minLimit, maxLimit) {
// 检查必须为整数
value = Math.floor(value)
if (value < minLimit) {
@@ -301,20 +219,19 @@ export default {
value = maxLimit
}
return value
},
// 隐藏弹窗
hidePopup() {
this.$emit("hide")
},
// 填充表达式
submitFill() {
this.$emit("fill", this.crontabValueString)
this.hidePopup()
},
clearCron() {
}
// 隐藏弹窗
function hidePopup() {
emit("hide")
}
// 填充表达式
function submitFill() {
emit("fill", crontabValueString.value)
hidePopup()
}
function clearCron() {
// 还原选择项
("准备还原")
this.crontabValueObj = {
crontabValueObj.value = {
second: "*",
min: "*",
hour: "*",
@@ -323,52 +240,14 @@ export default {
week: "?",
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>
<style scoped>
<style lang="scss" scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
@@ -376,7 +255,6 @@ export default {
.popup-main {
position: relative;
margin: 10px auto;
background: #fff;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
@@ -411,6 +289,11 @@ export default {
width: 100%;
margin: 0 auto;
}
.popup-result table td:not(.result) {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
.popup-result table span {
display: block;
width: 100%;
+89 -79
View File
@@ -1,116 +1,126 @@
<template>
<el-form size="small">
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
分钟允许的通配符[, - * /]
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
</el-radio>
</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='average02' :min="1" :max="59 - average01 || 0" /> 分钟执行一次
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
</el-radio>
</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-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
<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>
<script>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
name: 'crontab-min',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.min, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
this.$emit('update', 'min', '*', 'min')
emit('update', 'min', '*', 'min')
break
case 2:
this.$emit('update', 'min', this.cycleTotal, 'min')
emit('update', 'min', cycleTotal.value, 'min')
break
case 3:
this.$emit('update', 'min', this.averageTotal, 'min')
emit('update', 'min', averageTotal.value, 'min')
break
case 4:
this.$emit('update', 'min', this.checkboxString, 'min')
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'min', checkboxString.value, 'min')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'min', this.cycleTotal, 'min')
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'min', this.averageTotal, 'min')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'min', this.checkboxString, 'min')
}
},
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange',
},
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>
<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>
+102 -75
View File
@@ -1,114 +1,141 @@
<template>
<el-form size='small'>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="12" />
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" />
</el-radio>
</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='average02' :min="1" :max="12 - average01 || 0" /> 月月执行一次
<el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
</el-radio>
</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-option v-for="item in 12" :key="item" :value="item">{{item}}</el-option>
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
name: 'crontab-month',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const monthList = ref([
{key: 1, value: '一月'},
{key: 2, value: '二月'},
{key: 3, value: '三月'},
{key: 4, value: '四月'},
{key: 5, value: '五月'},
{key: 6, value: '六月'},
{key: 7, value: '七月'},
{key: 8, value: '八月'},
{key: 9, value: '九月'},
{key: 10, value: '十月'},
{key: 11, value: '十一月'},
{key: 12, value: '十二月'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 11)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 11)
average02.value = props.check(average02.value, 1, 12 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.month, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
this.$emit('update', 'month', '*')
emit('update', 'month', '*', 'month')
break
case 2:
this.$emit('update', 'month', this.cycleTotal)
emit('update', 'month', cycleTotal.value, 'month')
break
case 3:
this.$emit('update', 'month', this.averageTotal)
emit('update', 'month', averageTotal.value, 'month')
break
case 4:
this.$emit('update', 'month', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'month', checkboxString.value, 'month')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'month', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'month', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'month', this.checkboxString)
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 1, 11)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 12)
return cycle01 + '-' + cycle02
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 1, 11)
const average02 = this.checkNum(this.average02, 1, 12 - average01 || 0)
return average01 + '/' + average02
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join()
return str == '' ? '*' : str
}
}
}
</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>
+194 -212
View File
@@ -10,26 +10,25 @@
</div>
</template>
<script>
export default {
data() {
return {
dayRule: '',
dayRuleSup: '',
dateArr: [],
resultList: [],
isShow: false
<script setup>
const props = defineProps({
ex: {
type: String,
default: ''
}
},
name: 'crontab-result',
methods: {
// 表达式值变化时,开始去计算结果
expressionChange() {
})
const dayRule = ref('')
const dayRuleSup = ref('')
const dateArr = ref([])
const resultList = ref([])
const isShow = ref(false)
watch(() => props.ex, () => expressionChange())
// 表达式值变化时,开始去计算结果
function expressionChange() {
// 计算开始-隐藏结果
this.isShow = false
isShow.value = false
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = this.$options.propsData.ex.split(' ')
let ruleArr = props.ex.split(' ')
// 用于记录进入循环的次数
let nums = 0
// 用于暂时存符号时间规则结果的数组
@@ -43,27 +42,27 @@ export default {
let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds()
// 根据规则获取到近100年可能年数组、月数组等等
this.getSecondArr(ruleArr[0])
this.getMinArr(ruleArr[1])
this.getHourArr(ruleArr[2])
this.getDayArr(ruleArr[3])
this.getMonthArr(ruleArr[4])
this.getWeekArr(ruleArr[5])
this.getYearArr(ruleArr[6], nYear)
getSecondArr(ruleArr[0])
getMinArr(ruleArr[1])
getHourArr(ruleArr[2])
getDayArr(ruleArr[3])
getMonthArr(ruleArr[4])
getWeekArr(ruleArr[5])
getYearArr(ruleArr[6], nYear)
// 将获取到的数组赋值-方便使用
let sDate = this.dateArr[0]
let mDate = this.dateArr[1]
let hDate = this.dateArr[2]
let DDate = this.dateArr[3]
let MDate = this.dateArr[4]
let YDate = this.dateArr[5]
let sDate = dateArr.value[0]
let mDate = dateArr.value[1]
let hDate = dateArr.value[2]
let DDate = dateArr.value[3]
let MDate = dateArr.value[4]
let YDate = dateArr.value[5]
// 获取当前时间在数组中的索引
let sIdx = this.getIndex(sDate, nSecond)
let mIdx = this.getIndex(mDate, nMin)
let hIdx = this.getIndex(hDate, nHour)
let DIdx = this.getIndex(DDate, nDay)
let MIdx = this.getIndex(MDate, nMonth)
let YIdx = this.getIndex(YDate, nYear)
let sIdx = getIndex(sDate, nSecond)
let mIdx = getIndex(mDate, nMin)
let hIdx = getIndex(hDate, nHour)
let DIdx = getIndex(DDate, nDay)
let MIdx = getIndex(MDate, nMonth)
let YIdx = getIndex(YDate, nYear)
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function () {
sIdx = 0
@@ -109,7 +108,6 @@ export default {
if (nMin !== mDate[mIdx]) {
resetSecond()
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi]
@@ -126,7 +124,7 @@ export default {
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -137,13 +135,12 @@ export default {
// 赋值、方便后面运算
let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour()
if (Di == DDate.length - 1) {
if (Di === DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -151,59 +148,57 @@ export default {
}
continue
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && this.dayRule !== 'workDay' && this.dayRule !== 'lastWeek' && this.dayRule !== 'lastDay') {
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
resetDay()
continue goMonth
}
// 如果日期规则中有值时
if (this.dayRule == 'lastDay') {
if (dayRule.value === 'lastDay') {
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
} else if (this.dayRule == 'workDay') {
} else if (dayRule.value === 'workDay') {
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取达到条件的日期是星期X
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 当星期日时
if (thisWeek == 1) {
if (thisWeek === 1) {
// 先找下一个日,并判断是否为月底
DD++
thisDD = DD < 10 ? '0' + DD : DD
// 判断下一日已经不是合法日期
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3
}
} else if (thisWeek == 7) {
} else if (thisWeek === 7) {
// 当星期6时只需判断不是1号就可进行操作
if (this.dayRuleSup !== 1) {
if (dayRuleSup.value !== 1) {
DD--
} else {
DD += 2
}
}
} else if (this.dayRule == 'weekDay') {
} else if (dayRule.value === 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// 校验当前星期是否在星期池(dayRuleSup)中
if (this.dayRuleSup.indexOf(thisWeek) < 0) {
if (dayRuleSup.value.indexOf(thisWeek) < 0) {
// 如果到达最大值时
if (Di == DDate.length - 1) {
if (Di === DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -211,48 +206,46 @@ export default {
}
continue
}
} else if (this.dayRule == 'assWeek') {
} else if (dayRule.value === 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (this.dayRuleSup[1] >= thisWeek) {
DD = (this.dayRuleSup[0] - 1) * 7 + this.dayRuleSup[1] - thisWeek + 1
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (dayRuleSup.value[1] >= thisWeek) {
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
} else {
DD = this.dayRuleSup[0] * 7 + this.dayRuleSup[1] - thisWeek + 1
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
}
} else if (this.dayRule == 'lastWeek') {
} else if (dayRule.value === 'lastWeek') {
// 如果指定了每月最后一个星期几
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取月末最后一天是星期几
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 找到要求中最近的那个星期几
if (this.dayRuleSup < thisWeek) {
DD -= thisWeek - this.dayRuleSup
} else if (this.dayRuleSup > thisWeek) {
DD -= 7 - (this.dayRuleSup - thisWeek)
if (dayRuleSup.value < thisWeek) {
DD -= thisWeek - dayRuleSup.value
} else if (dayRuleSup.value > thisWeek) {
DD -= 7 - (dayRuleSup.value - thisWeek)
}
}
// 判断时间值是否小于10置换成“05”这种格式
DD = DD < 10 ? '0' + DD : DD
// 循环“时”数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin()
if (hi == hDate.length - 1) {
if (hi === hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
if (Di === DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -265,17 +258,16 @@ export default {
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond()
if (mi == mDate.length - 1) {
if (mi === mDate.length - 1) {
resetMin()
if (hi == hDate.length - 1) {
if (hi === hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
if (Di === DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -296,17 +288,17 @@ export default {
nums++
}
// 如果条数满了就退出循环
if (nums == 5) break goYear
if (nums === 5) break goYear
// 如果到达最大值时
if (si == sDate.length - 1) {
if (si === sDate.length - 1) {
resetSecond()
if (mi == mDate.length - 1) {
if (mi === mDate.length - 1) {
resetMin()
if (hi == hDate.length - 1) {
if (hi === hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
if (Di === DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
@@ -325,21 +317,19 @@ export default {
}//goMonth
}
// 判断100年内的结果条数
if (resultArr.length == 0) {
this.resultList = ['没有达到条件的结果!']
if (resultArr.length === 0) {
resultList.value = ['没有达到条件的结果!']
} else {
this.resultList = resultArr
resultList.value = resultArr
if (resultArr.length !== 5) {
this.resultList.push('最近100年内只有上面' + resultArr.length + '条结果!')
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
}
}
// 计算完成-显示结果
this.isShow = true
},
// 用于计算某位数字在数组中的索引
getIndex(arr, value) {
isShow.value = true
}
// 用于计算某位数字在数组中的索引
function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0
} else {
@@ -349,138 +339,138 @@ export default {
}
}
}
},
// 获取"年"数组
getYearArr(rule, year) {
this.dateArr[5] = this.getOrderArr(year, year + 100)
}
// 获取"年"数组
function getYearArr(rule, year) {
dateArr.value[5] = getOrderArr(year, year + 100)
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
this.dateArr[5] = this.getCycleArr(rule, year + 100, false)
dateArr.value[5] = getCycleArr(rule, year + 100, false)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[5] = this.getAverageArr(rule, year + 100)
dateArr.value[5] = getAverageArr(rule, year + 100)
} else if (rule !== '*') {
this.dateArr[5] = this.getAssignArr(rule)
dateArr.value[5] = getAssignArr(rule)
}
}
},
// 获取"月"数组
getMonthArr(rule) {
this.dateArr[4] = this.getOrderArr(1, 12)
}
// 获取"月"数组
function getMonthArr(rule) {
dateArr.value[4] = getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) {
this.dateArr[4] = this.getCycleArr(rule, 12, false)
dateArr.value[4] = getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[4] = this.getAverageArr(rule, 12)
dateArr.value[4] = getAverageArr(rule, 12)
} else if (rule !== '*') {
this.dateArr[4] = this.getAssignArr(rule)
dateArr.value[4] = getAssignArr(rule)
}
},
// 获取"日"数组-主要为日期规则
getWeekArr(rule) {
}
// 获取"日"数组-主要为日期规则
function getWeekArr(rule) {
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
if (this.dayRule == '' && this.dayRuleSup == '') {
if (dayRule.value === '' && dayRuleSup.value === '') {
if (rule.indexOf('-') >= 0) {
this.dayRule = 'weekDay'
this.dayRuleSup = this.getCycleArr(rule, 7, false)
dayRule.value = 'weekDay'
dayRuleSup.value = getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) {
this.dayRule = 'assWeek'
dayRule.value = 'assWeek'
let matchRule = rule.match(/[0-9]{1}/g)
this.dayRuleSup = [Number(matchRule[1]), Number(matchRule[0])]
this.dateArr[3] = [1]
if (this.dayRuleSup[1] == 7) {
this.dayRuleSup[1] = 0
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
dateArr.value[3] = [1]
if (dayRuleSup.value[1] === 7) {
dayRuleSup.value[1] = 0
}
} else if (rule.indexOf('L') >= 0) {
this.dayRule = 'lastWeek'
this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0])
this.dateArr[3] = [31]
if (this.dayRuleSup == 7) {
this.dayRuleSup = 0
dayRule.value = 'lastWeek'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [31]
if (dayRuleSup.value === 7) {
dayRuleSup.value = 0
}
} else if (rule !== '*' && rule !== '?') {
this.dayRule = 'weekDay'
this.dayRuleSup = this.getAssignArr(rule)
dayRule.value = 'weekDay'
dayRuleSup.value = getAssignArr(rule)
}
}
},
// 获取"日"数组-少量为日期规则
getDayArr(rule) {
this.dateArr[3] = this.getOrderArr(1, 31)
this.dayRule = ''
this.dayRuleSup = ''
}
// 获取"日"数组-少量为日期规则
function getDayArr(rule) {
dateArr.value[3] = getOrderArr(1, 31)
dayRule.value = ''
dayRuleSup.value = ''
if (rule.indexOf('-') >= 0) {
this.dateArr[3] = this.getCycleArr(rule, 31, false)
this.dayRuleSup = 'null'
dateArr.value[3] = getCycleArr(rule, 31, false)
dayRuleSup.value = 'null'
} else if (rule.indexOf('/') >= 0) {
this.dateArr[3] = this.getAverageArr(rule, 31)
this.dayRuleSup = 'null'
dateArr.value[3] = getAverageArr(rule, 31)
dayRuleSup.value = 'null'
} else if (rule.indexOf('W') >= 0) {
this.dayRule = 'workDay'
this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0])
this.dateArr[3] = [this.dayRuleSup]
dayRule.value = 'workDay'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [dayRuleSup.value]
} else if (rule.indexOf('L') >= 0) {
this.dayRule = 'lastDay'
this.dayRuleSup = 'null'
this.dateArr[3] = [31]
dayRule.value = 'lastDay'
dayRuleSup.value = 'null'
dateArr.value[3] = [31]
} else if (rule !== '*' && rule !== '?') {
this.dateArr[3] = this.getAssignArr(rule)
this.dayRuleSup = 'null'
} else if (rule == '*') {
this.dayRuleSup = 'null'
dateArr.value[3] = getAssignArr(rule)
dayRuleSup.value = 'null'
} else if (rule === '*') {
dayRuleSup.value = 'null'
}
},
// 获取"时"数组
getHourArr(rule) {
this.dateArr[2] = this.getOrderArr(0, 23)
}
// 获取"时"数组
function getHourArr(rule) {
dateArr.value[2] = getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) {
this.dateArr[2] = this.getCycleArr(rule, 24, true)
dateArr.value[2] = getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[2] = this.getAverageArr(rule, 23)
dateArr.value[2] = getAverageArr(rule, 23)
} else if (rule !== '*') {
this.dateArr[2] = this.getAssignArr(rule)
dateArr.value[2] = getAssignArr(rule)
}
},
// 获取"分"数组
getMinArr(rule) {
this.dateArr[1] = this.getOrderArr(0, 59)
}
// 获取"分"数组
function getMinArr(rule) {
dateArr.value[1] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
this.dateArr[1] = this.getCycleArr(rule, 60, true)
dateArr.value[1] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[1] = this.getAverageArr(rule, 59)
dateArr.value[1] = getAverageArr(rule, 59)
} else if (rule !== '*') {
this.dateArr[1] = this.getAssignArr(rule)
dateArr.value[1] = getAssignArr(rule)
}
},
// 获取"秒"数组
getSecondArr(rule) {
this.dateArr[0] = this.getOrderArr(0, 59)
}
// 获取"秒"数组
function getSecondArr(rule) {
dateArr.value[0] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
this.dateArr[0] = this.getCycleArr(rule, 60, true)
dateArr.value[0] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[0] = this.getAverageArr(rule, 59)
dateArr.value[0] = getAverageArr(rule, 59)
} else if (rule !== '*') {
this.dateArr[0] = this.getAssignArr(rule)
dateArr.value[0] = getAssignArr(rule)
}
},
// 根据传进来的min-max返回一个顺序的数组
getOrderArr(min, max) {
}
// 根据传进来的min-max返回一个顺序的数组
function getOrderArr(min, max) {
let arr = []
for (let i = min; i <= max; i++) {
arr.push(i)
}
return arr
},
// 根据规则中指定的零散值返回一个数组
getAssignArr(rule) {
}
// 根据规则中指定的零散值返回一个数组
function getAssignArr(rule) {
let arr = []
let assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i])
}
arr.sort(this.compare)
arr.sort(compare)
return arr
},
// 根据一定算术规则计算返回一个数组
getAverageArr(rule, limit) {
}
// 根据一定算术规则计算返回一个数组
function getAverageArr(rule, limit) {
let arr = []
let agArr = rule.split('/')
let min = Number(agArr[0])
@@ -490,9 +480,9 @@ export default {
min += step
}
return arr
},
// 根据规则返回一个具有周期性的数组
getCycleArr(rule, limit, status) {
}
// 根据规则返回一个具有周期性的数组
function getCycleArr(rule, limit, status) {
// status--表示是否从0开始(则从1开始)
let arr = []
let cycleArr = rule.split('-')
@@ -503,24 +493,24 @@ export default {
}
for (let i = min; i <= max; i++) {
let add = 0
if (status == false && i % limit == 0) {
if (status === false && i % limit === 0) {
add = limit
}
arr.push(Math.round(i % limit + add))
}
arr.sort(this.compare)
arr.sort(compare)
return arr
},
// 比较数字大小(用于Array.sort
compare(value1, value2) {
}
// 比较数字大小(用于Array.sort
function compare(value1, value2) {
if (value2 - value1 > 0) {
return -1
} else {
return 1
}
},
// 格式化日期格式如:2017-9-19 18:04:33
formatDate(value, type) {
}
// 格式化日期格式如:2017-9-19 18:04:33
function formatDate(value, type) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value
let Y = time.getFullYear()
@@ -531,28 +521,20 @@ export default {
let s = time.getSeconds()
let week = time.getDay()
// 如果传递了type的话
if (type == undefined) {
if (type === undefined) {
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
} else if (type == 'week') {
} else if (type === 'week') {
// 在quartz中 1为星期日
return week + 1
}
},
// 检查日期是否存在
checkDate(value) {
let time = new Date(value)
let format = this.formatDate(time)
return value === format
}
},
watch: {
'ex': 'expressionChange'
},
props: ['ex'],
mounted: function () {
// 初始化 获取一次结果
this.expressionChange()
}
}
// 检查日期是否存在
function checkDate(value) {
let time = new Date(value)
let format = formatDate(time)
return value === format
}
onMounted(() => {
expressionChange()
})
</script>
+89 -78
View File
@@ -1,117 +1,128 @@
<template>
<el-form size="small">
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" />
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" />
</el-radio>
</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='average02' :min="1" :max="59 - average01 || 0" /> 秒执行一次
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
</el-radio>
</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-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
<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>
<script>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
name: 'crontab-second',
props: ['check', 'radioParent'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.second, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
// 单选按钮值变化时
function onRadioChange() {
switch (radioValue.value) {
case 1:
this.$emit('update', 'second', '*', 'second')
emit('update', 'second', '*', 'second')
break
case 2:
this.$emit('update', 'second', this.cycleTotal)
emit('update', 'second', cycleTotal.value, 'second')
break
case 3:
this.$emit('update', 'second', this.averageTotal)
emit('update', 'second', averageTotal.value, 'second')
break
case 4:
this.$emit('update', 'second', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'second', checkboxString.value, 'second')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'second', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'second', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'second', this.checkboxString)
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange',
radioParent() {
this.radioValue = this.radioParent
}
},
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>
<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>
+125 -130
View File
@@ -1,27 +1,27 @@
<template>
<el-form size='small'>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="1">
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L #]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="2">
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</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-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key === 1"
:disabled="item.key === 7"
>{{item.value}}</el-option>
</el-select>
-
@@ -31,36 +31,36 @@
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key < cycle01 && item.key !== 1"
:disabled="item.key <= cycle01"
>{{item.value}}</el-option>
</el-select>
</el-radio>
</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-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-radio>
</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-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-radio>
</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-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="String(item.key)">{{item.value}}</el-option>
<el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
@@ -68,135 +68,130 @@
</el-form>
</template>
<script>
export default {
data() {
return {
radioValue: 2,
weekday: 2,
cycle01: 2,
cycle02: 3,
average01: 1,
average02: 2,
checkboxList: [],
weekList: [
{
key: 2,
value: '星期一'
},
{
key: 3,
value: '星期二'
},
{
key: 4,
value: '星期三'
},
{
key: 5,
value: '星期四'
},
{
key: 6,
value: '星期五'
},
{
key: 7,
value: '星期六'
},
{
key: 1,
value: '星期日'
}
],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
name: 'crontab-week',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue !== 2 && this.cron.day !== '?') {
this.$emit('update', 'day', '?', 'week')
check: {
type: Function,
default: () => {
}
switch (this.radioValue) {
}
})
const radioValue = ref(2)
const cycle01 = ref(2)
const cycle02 = ref(3)
const average01 = ref(1)
const average02 = ref(2)
const weekday = ref(2)
const checkboxList = ref([])
const checkCopy = ref([2])
const weekList = ref([
{key: 1, value: '星期日'},
{key: 2, value: '星期一'},
{key: 3, value: '星期二'},
{key: 4, value: '星期三'},
{key: 5, value: '星期四'},
{key: 6, value: '星期五'},
{key: 7, value: '星期六'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 6)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 4)
average02.value = props.check(average02.value, 1, 7)
return average02.value + '#' + average01.value
})
const weekdayTotal = computed(() => {
weekday.value = props.check(weekday.value, 1, 7)
return weekday.value + 'L'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.week, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("#") > -1) {
const indexArr = value.split('#')
average01.value = Number(indexArr[1])
average02.value = Number(indexArr[0])
radioValue.value = 4
} else if (value.indexOf("L") > -1) {
const indexArr = value.split("L")
weekday.value = Number(indexArr[0])
radioValue.value = 5
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 6
}
}
function onRadioChange() {
if (radioValue.value === 2 && props.cron.day === '?') {
emit('update', 'day', '*', 'week')
}
if (radioValue.value !== 2 && props.cron.day !== '?') {
emit('update', 'day', '?', 'week')
}
switch (radioValue.value) {
case 1:
this.$emit('update', 'week', '*')
emit('update', 'week', '*', 'week')
break
case 2:
this.$emit('update', 'week', '?')
emit('update', 'week', '?', 'week')
break
case 3:
this.$emit('update', 'week', this.cycleTotal)
emit('update', 'week', cycleTotal.value, 'week')
break
case 4:
this.$emit('update', 'week', this.averageTotal)
emit('update', 'week', averageTotal.value, 'week')
break
case 5:
this.$emit('update', 'week', this.weekdayCheck + 'L')
emit('update', 'week', weekdayTotal.value, 'week')
break
case 6:
this.$emit('update', 'week', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'week', checkboxString.value, 'week')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'week', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'week', this.averageTotal)
}
},
// 最近工作日值变化时
weekdayChange() {
if (this.radioValue == '5') {
this.$emit('update', 'week', this.weekday + 'L')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '6') {
this.$emit('update', 'week', this.checkboxString)
}
},
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'weekdayCheck': 'weekdayChange',
'checkboxString': 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function () {
this.cycle01 = this.checkNum(this.cycle01, 1, 7)
this.cycle02 = this.checkNum(this.cycle02, 1, 7)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
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>
<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>
+96 -84
View File
@@ -1,38 +1,38 @@
<template>
<el-form size="small">
<el-form>
<el-form-item>
<el-radio :label="1" v-model='radioValue'>
<el-radio :value="1" v-model='radioValue'>
不填允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="2" v-model='radioValue'>
<el-radio :value="2" v-model='radioValue'>
每年
</el-radio>
</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='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099" />
<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-radio>
</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='average02' :min="1" :max="2099 - average01 || fullYear" /> 年执行一次
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
</el-radio>
</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-select>
</el-radio>
@@ -40,92 +40,104 @@
</el-form>
</template>
<script>
export default {
data() {
return {
fullYear: 0,
radioValue: 1,
cycle01: 0,
cycle02: 0,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
name: 'crontab-year',
props: ['check', 'month', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
check: {
type: Function,
default: () => {
}
}
})
const fullYear = Number(new Date().getFullYear())
const maxFullYear = fullYear + 10
const radioValue = ref(1)
const cycle01 = ref(fullYear)
const cycle02 = ref(fullYear + 1)
const average01 = ref(fullYear)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([fullYear])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.year, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '') {
radioValue.value = 1
} else if (value === "*") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 5
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
this.$emit('update', 'year', '')
emit('update', 'year', '', 'year')
break
case 2:
this.$emit('update', 'year', '*')
emit('update', 'year', '*', 'year')
break
case 3:
this.$emit('update', 'year', this.cycleTotal)
emit('update', 'year', cycleTotal.value, 'year')
break
case 4:
this.$emit('update', 'year', this.averageTotal)
emit('update', 'year', averageTotal.value, 'year')
break
case 5:
this.$emit('update', 'year', this.checkboxString)
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'year', checkboxString.value, 'year')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'year', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'year', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '5') {
this.$emit('update', 'year', this.checkboxString)
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
computed: {
// 计算两个周期值
cycleTotal: function () {
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
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, this.fullYear, 2098)
const average02 = this.checkNum(this.average02, 1, 2099 - average01 || this.fullYear)
return average01 + '/' + average02
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join()
return str
}
},
mounted: function () {
// 仅获取当前年份
this.fullYear = Number(new Date().getFullYear())
this.cycle01 = this.fullYear
this.average01 = this.fullYear
}
}
</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,
}
+40 -46
View File
@@ -3,22 +3,19 @@
<template v-for="(item, index) in options">
<template v-if="isValueMatch(item.value)">
<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"
:index="index"
:class="item.raw.cssClass"
>{{ item.label + ' ' }}</span
>
:class="item.elTagClass"
>{{ item.label + " " }}</span>
<el-tag
v-else
:disable-transitions="true"
:key="item.value"
:key="item.value + ''"
:index="index"
:type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
:class="item.raw.cssClass"
>
{{ item.label + ' ' }}
</el-tag>
:type="item.elTagType"
:class="item.elTagClass"
>{{ item.label + " " }}</el-tag>
</template>
</template>
<template v-if="unmatch && showValue">
@@ -27,65 +24,62 @@
</div>
</template>
<script>
export default {
name: "DictTag",
props: {
<script setup>
// 记录未匹配的项
const unmatchArray = ref([])
const props = defineProps({
// 数据
options: {
type: Array,
default: null,
},
// 当前的值
value: [Number, String, Array],
// 当未找到匹配的数据时,显示value
showValue: {
type: Boolean,
default: true,
},
separator: {
type: String,
default: ","
default: ",",
}
},
data() {
return {
unmatchArray: [], // 记录未匹配的项
}
},
computed: {
values() {
if (this.value === null || typeof this.value === 'undefined' || this.value === '') return []
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 = []
})
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 (this.value === null || typeof this.value === 'undefined' || this.value === '' || this.options.length === 0) return false
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
this.values.forEach(item => {
if (!this.options.some(v => v.value == item)) {
this.unmatchArray.push(item)
values.value.forEach(item => {
if (!props.options.some(v => v.value == item)) {
unmatchArray.value.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 ''
})
function handleArray(array) {
if (array.length === 0) return ""
return array.reduce((pre, cur) => {
return pre + ' ' + cur
return pre + " " + cur
})
},
}
}
function isValueMatch(itemValue) {
return values.value.some(val => val == itemValue)
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
+92 -113
View File
@@ -8,30 +8,42 @@
name="file"
:show-file-list="false"
:headers="headers"
style="display: none"
ref="upload"
v-if="this.type == 'url'"
class="editor-img-uploader"
v-if="type == 'url'"
>
<i ref="uploadRef" class="editor-img-uploader"></i>
</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>
</template>
<script>
import axios from "axios"
import Quill from "quill"
import "quill/dist/quill.core.css"
import "quill/dist/quill.snow.css"
import "quill/dist/quill.bubble.css"
<script setup>
import axios from 'axios'
import { QuillEditor } from "@vueup/vue-quill"
import "@vueup/vue-quill/dist/vue-quill.snow.css"
import { getToken } from "@/utils/auth"
export default {
name: "Editor",
props: {
const { proxy } = getCurrentInstance()
const quillEditorRef = ref()
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
const headers = ref({
Authorization: "Bearer " + getToken()
})
const props = defineProps({
/* 编辑器的内容 */
value: {
modelValue: {
type: String,
default: "",
},
/* 高度 */
height: {
@@ -58,16 +70,9 @@ export default {
type: String,
default: "url",
}
},
data() {
return {
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken()
},
Quill: null,
currentValue: "",
options: {
})
const options = ref({
theme: "snow",
bounds: document.body,
debug: "warn",
@@ -87,115 +92,87 @@ export default {
],
},
placeholder: "请输入内容",
readOnly: this.readOnly,
},
}
},
computed: {
styles() {
readOnly: props.readOnly
})
const styles = computed(() => {
let style = {}
if (this.minHeight) {
style.minHeight = `${this.minHeight}px`
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`
}
if (this.height) {
style.height = `${this.height}px`
if (props.height) {
style.height = `${props.height}px`
}
return style
})
const content = ref("")
watch(() => props.modelValue, (v) => {
if (v !== content.value) {
content.value = v == undefined ? "<p></p>" : v
}
},
watch: {
value: {
handler(val) {
if (val !== this.currentValue) {
this.currentValue = val === null ? "" : val
if (this.Quill) {
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue)
}
}
},
immediate: true,
},
},
mounted() {
this.init()
},
beforeDestroy() {
this.Quill = null
},
methods: {
init() {
const editor = this.$refs.editor
this.Quill = new Quill(editor, this.options)
// 如果设置了上传地址则自定义图片上传事件
if (this.type == 'url') {
let toolbar = this.Quill.getModule("toolbar")
}, { immediate: true })
// 如果设置了上传地址则自定义图片上传事件
onMounted(() => {
if (props.type == 'url') {
let quill = quillEditorRef.value.getQuill()
let toolbar = quill.getModule("toolbar")
toolbar.addHandler("image", (value) => {
if (value) {
this.$refs.upload.$children[0].$refs.input.click()
proxy.$refs.uploadRef.click()
} else {
this.quill.format("image", false)
quill.format("image", false)
}
})
this.Quill.root.addEventListener('paste', this.handlePasteCapture, true)
quill.root.addEventListener('paste', handlePasteCapture, true)
}
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue)
this.Quill.on("text-change", (delta, oldDelta, source) => {
const html = this.$refs.editor.children[0].innerHTML
const text = this.Quill.getText()
const quill = this.Quill
this.currentValue = html
this.$emit("input", html)
this.$emit("on-change", { html, text, quill })
})
this.Quill.on("text-change", (delta, oldDelta, source) => {
this.$emit("on-text-change", delta, oldDelta, source)
})
this.Quill.on("selection-change", (range, oldRange, source) => {
this.$emit("on-selection-change", range, oldRange, source)
})
this.Quill.on("editor-change", (eventName, ...args) => {
this.$emit("on-editor-change", eventName, ...args)
})
},
// 上传前校检格式和大小
handleBeforeUpload(file) {
})
// 上传前校检格式和大小
function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
const isJPG = type.includes(file.type)
// 检验文件格式
//检验文件格式
if (!isJPG) {
this.$message.error(`图片格式错误!`)
proxy.$modal.msgError(`图片格式错误!`)
return false
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`)
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
return true
},
handleUploadSuccess(res, file) {
}
// 上传成功处理
function 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)
// 获取富文本实例
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 {
this.$message.error("图片插入失败")
proxy.$modal.msgError("图片插入失败")
}
},
handleUploadError() {
this.$message.error("图片插入失败")
},
// 复制粘贴图片处理
handlePasteCapture(e) {
}
// 上传失败处理
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++) {
@@ -203,23 +180,25 @@ export default {
if (item.type.indexOf('image') !== -1) {
e.preventDefault()
const file = item.getAsFile()
this.insertImage(file)
insertImage(file)
}
}
}
},
insertImage(file) {
}
function insertImage(file) {
const formData = new FormData()
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)
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
handleUploadSuccess(res.data)
})
}
}
}
</script>
<style>
.editor-img-uploader {
display: none;
}
.editor, .ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
@@ -1,28 +1,33 @@
<template>
<el-dialog :title="title" :visible.sync="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>
<i class="el-icon-upload"></i>
<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-change="handleFileChange" :on-remove="handleFileRemove" :on-success="handleSuccess" :auto-upload="false" drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip text-center" slot="tip">
<div class="el-upload__tip" slot="tip">
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
</div>
<span>仅允许导入xlsxlsx格式文件</span>
<el-link v-if="templateUrl" type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
<el-link v-if="templateUrl" type="primary" underline="never" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<div slot="footer" class="dialog-footer">
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script>
<script setup>
import { getToken } from '@/utils/auth'
export default {
props: {
const { proxy } = getCurrentInstance()
const props = defineProps({
// 对话框标题
title: {
type: String,
@@ -43,7 +48,7 @@ export default {
type: String,
default: ''
},
// 模板文件名
// 模板文件名前缀
templateFileName: {
type: String,
default: 'template'
@@ -53,74 +58,80 @@ export default {
type: String,
default: '是否更新已经存在的数据'
}
},
data() {
return {
visible: false,
isUploading: false,
updateSupport: false,
headers: { Authorization: 'Bearer ' + getToken() }
}
},
computed: {
uploadUrl() {
return process.env.VUE_APP_BASE_API + this.action + '?updateSupport=' + (this.updateSupport ? 1 : 0)
},
templateUrl() {
return !!this.templateAction
}
},
methods: {
// 打开对话框(供父组件通过 ref 调用)
open() {
this.updateSupport = false
this.isUploading = false
this.visible = true
this.$nextTick(() => {
if (this.$refs.uploadRef) {
this.$refs.uploadRef.clearFiles()
}
})
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()
})
},
// 关闭时清理
handleClose() {
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()
}
}
}
// 关闭时清理
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>
+106 -112
View File
@@ -17,39 +17,35 @@
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button>
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
<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>
</el-upload>
<!-- 文件列表 -->
<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">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<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="never" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<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">&nbsp;删除</el-link>
</div>
</li>
</transition-group>
</div>
</template>
<script>
<script setup>
import { getToken } from "@/utils/auth"
import Sortable from 'sortablejs'
export default {
name: "FileUpload",
props: {
// 值
value: [String, Object, Array],
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
@@ -89,43 +85,27 @@ export default {
type: Boolean,
default: true
}
},
data() {
return {
number: 0,
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) {
this.$nextTick(() => {
const element = this.$refs.uploadFileList?.$el || this.$refs.uploadFileList
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: {
value: {
handler(val) {
})
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 : this.value.split(',')
const list = Array.isArray(val) ? val : props.modelValue.split(',')
// 然后将数组转为对象数组
this.fileList = list.map(item => {
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item }
}
@@ -133,109 +113,122 @@ export default {
return item
})
} else {
this.fileList = []
fileList.value = []
return []
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize)
},
},
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
},{ deep: true, immediate: true })
// 上传前校检格式和大小
function handleBeforeUpload(file) {
// 校检文件类型
if (this.fileType) {
if (props.fileType.length) {
const fileName = file.name.split('.')
const fileExt = fileName[fileName.length - 1]
const isTypeOk = this.fileType.indexOf(fileExt) >= 0
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}格式文件!`)
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
return false
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
this.$modal.msgError('文件名不正确,不能包含英文逗号!')
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`)
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
this.$modal.loading("正在上传文件,请稍候...")
this.number++
proxy.$modal.loading("正在上传文件,请稍候...")
number.value++
return true
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`)
},
// 上传失败
handleUploadError(err) {
this.$modal.msgError("上传文件失败,请重试")
this.$modal.closeLoading()
},
// 上传成功回调
handleUploadSuccess(res, file) {
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传失败
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败")
proxy.$modal.closeLoading()
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName })
this.uploadedSuccessfully()
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
this.number--
this.$modal.closeLoading()
this.$modal.msgError(res.msg)
this.$refs.fileUpload.handleRemove(file)
this.uploadedSuccessfully()
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.fileUpload.handleRemove(file)
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()
}
// 删除文件
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()
}
},
// 获取文件名称
getFileName(name) {
}
// 获取文件名称
function getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1)
} else {
return name
}
},
// 对象转成指定字符串分隔
listToString(list, separator) {
}
// 对象转成指定字符串分隔
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) : ''
}
}
}
</script>
// 初始化拖拽排序
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">
.file-upload-darg {
opacity: 0.5;
@@ -249,6 +242,7 @@ export default {
line-height: 2;
margin-bottom: 10px;
position: relative;
transition: none !important;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
+8 -10
View File
@@ -7,26 +7,24 @@
xmlns="http://www.w3.org/2000/svg"
width="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" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
<script setup>
defineProps({
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick')
}
</script>
+131 -131
View File
@@ -2,8 +2,8 @@
<div class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-dialog
:visible.sync="show"
width="600px"
v-model="show"
width="600"
@close="close"
@opened="onDialogOpened"
:show-close="false"
@@ -14,12 +14,12 @@
ref="headerSearchSelectRef"
size="large"
@input="querySearch"
prefix-icon="el-icon-search"
prefix-icon="Search"
placeholder="菜单搜索,支持标题、URL模糊查询"
clearable
@keyup.enter.native="selectActiveResult"
@keydown.up.native="navigateResult('up')"
@keydown.down.native="navigateResult('down')"
@keyup.enter="selectActiveResult"
@keydown.up.prevent="navigateResult('up')"
@keydown.down.prevent="navigateResult('down')"
>
</el-input>
@@ -27,11 +27,13 @@
找到 <strong>{{ options.length }}</strong> 个结果
</div>
<el-scrollbar wrap-class="right-scrollbar-wrapper">
<div class="result-wrap">
<el-scrollbar>
<template v-if="options.length > 0">
<div
class="search-item"
tabindex="1"
v-for="(item, index) in options"
:key="item.path"
:class="{ 'is-active': index === activeIndex }"
@@ -51,13 +53,13 @@
</template>
<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-tip">试试其他关键词或路径</p>
</div>
</div>
</el-scrollbar>
</div>
<div class="search-footer">
<span class="shortcut-item">
@@ -74,85 +76,71 @@
</div>
</template>
<script>
import Fuse from 'fuse.js/dist/fuse.min.js'
import path from 'path'
<script setup>
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
activeIndex: -1,
show: false,
fuse: undefined
const search = ref('')
const options = ref([])
const searchPool = ref([])
const activeIndex = ref(-1)
const show = ref(false)
const fuse = ref(undefined)
const headerSearchSelectRef = ref(null)
const router = useRouter()
const theme = computed(() => useSettingsStore().theme)
const routes = computed(() => usePermissionStore().defaultRoutes)
function click() {
show.value = !show.value
if (show.value) {
options.value = searchPool.value
}
},
computed: {
theme() {
return this.$store.state.settings.theme
},
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()
}
function onDialogOpened() {
nextTick(() => {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
})
},
close() {
this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.blur()
this.search = ''
this.options = this.searchPool
this.show = false
this.activeIndex = -1
},
change(val) {
}
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(val.path)) {
if (isHttp(p)) {
// http(s):// 路径新窗口打开
const pindex = p.indexOf('http')
window.open(p.substr(pindex, p.length), '_blank')
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) })
router.push({ path: p, query: JSON.parse(query) })
} else {
this.$router.push(p)
router.push(p)
}
}
this.search = ''
this.options = this.searchPool
this.$nextTick(() => {
this.show = false
search.value = ''
options.value = searchPool.value
nextTick(() => {
show.value = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
threshold: 0.2,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
@@ -162,102 +150,114 @@ export default {
weight: 0.3
}]
})
},
generateRoutes(routes, basePath = '/', prefixTitle = []) {
}
function generateRoutes(routes, basePath = '', prefixTitle = []) {
let res = []
for (const router of routes) {
if (router.hidden) { continue }
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(router.path) ? path.resolve(basePath, router.path) : router.path,
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.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') {
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 (router.query) {
data.query = router.query
if (r.query) {
data.query = r.query
}
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (r.children) {
const tempRoutes = generateRoutes(r.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
this.activeIndex = -1
}
function querySearch(query) {
activeIndex.value = -1
if (query !== '') {
const q = query.toLowerCase()
const pathMatches = this.searchPool.filter(item =>
const pathMatches = searchPool.value.filter(item =>
item.path.toLowerCase().includes(q)
)
const fuseMatches = this.fuse.search(query).map(item => item.item)
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)
}
})
this.options = merged
options.value = 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, '\\$&')
}
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>
<style lang='scss' scoped>
::v-deep {
.el-dialog__header {
:deep(.el-dialog__header) {
padding: 6px !important;
}
}
.highlight {
:deep(.highlight) {
color: red;
font-weight: 600;
}
}
.is-active .highlight {
:deep(.is-active .highlight) {
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
}
}
.header-search {
+37 -30
View File
@@ -1,8 +1,14 @@
<!-- @author zhengjie -->
<template>
<div class="icon-body">
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
<i slot="suffix" class="el-icon-search el-input__icon" />
<el-input
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>
<div class="icon-list">
<div class="list-container">
@@ -17,41 +23,42 @@
</div>
</template>
<script>
<script setup>
import icons from './requireIcons'
export default {
name: 'IconSelect',
props: {
const props = defineProps({
activeIcon: {
type: String
}
},
data() {
return {
name: '',
iconList: icons
}
},
methods: {
filterIcons() {
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
}
})
const iconName = ref('')
const iconList = ref(icons)
const emit = defineEmits(['selected'])
function filterIcons() {
iconList.value = icons
if (iconName.value) {
iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
}
}
function selectedIcon(name) {
emit('selected', name)
document.body.click()
}
function reset() {
iconName.value = ''
iconList.value = icons
}
defineExpose({
reset
})
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
<style lang='scss' scoped>
.icon-body {
width: 100%;
padding: 10px;
@@ -1,11 +1,8 @@
const req = require.context('../../assets/icons/svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys()
const re = /\.\/(.*)\.svg/
const icons = requireAll(req).map(i => {
return i.match(re)[1]
})
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)
}
export default icons
+30 -28
View File
@@ -4,19 +4,20 @@
fit="cover"
:style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList"
preview-teleported
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
<template #error>
<div class="image-slot">
<el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image>
</template>
<script>
<script setup>
import { isExternal } from "@/utils/validate"
export default {
name: "ImagePreview",
props: {
const props = defineProps({
src: {
type: String,
default: ""
@@ -29,40 +30,41 @@ export default {
type: [Number, String],
default: ""
}
},
computed: {
realSrc() {
if (!this.src) {
})
const realSrc = computed(() => {
if (!props.src) {
return
}
let real_src = this.src.split(",")[0]
let real_src = props.src.split(",")[0]
if (isExternal(real_src)) {
return real_src
}
return process.env.VUE_APP_BASE_API + real_src
},
realSrcList() {
if (!this.src) {
return import.meta.env.VITE_APP_BASE_API + real_src
})
const realSrcList = computed(() => {
if (!props.src) {
return
}
let real_src_list = this.src.split(",")
let real_src_list = props.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.push(import.meta.env.VITE_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 realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
)
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
)
</script>
<style lang="scss" scoped>
@@ -70,14 +72,14 @@ export default {
border-radius: 5px;
background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc;
::v-deep .el-image__inner {
:deep(.el-image__inner) {
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
::v-deep .image-slot {
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;
+123 -137
View File
@@ -12,28 +12,31 @@
:on-error="handleUploadError"
:on-exceed="handleExceed"
ref="imageUpload"
:on-remove="handleDelete"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
: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>
<!-- 上传提示 -->
<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="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
<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-dialog
:visible.sync="dialogVisible"
v-model="dialogVisible"
title="预览"
width="800"
width="800px"
append-to-body
>
<img
@@ -44,14 +47,13 @@
</div>
</template>
<script>
<script setup>
import { getToken } from "@/utils/auth"
import { isExternal } from "@/utils/validate"
import Sortable from 'sortablejs'
export default {
props: {
value: [String, Object, Array],
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
@@ -91,47 +93,31 @@ export default {
type: Boolean,
default: true
}
},
data() {
return {
number: 0,
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) {
this.$nextTick(() => {
const element = this.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
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: {
value: {
handler(val) {
})
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref("")
const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = 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) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',')
const list = Array.isArray(val) ? val : props.modelValue.split(",")
// 然后将数组转为对象数组
this.fileList = list.map(item => {
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) {
item = { name: this.baseUrl + item, url: this.baseUrl + item }
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item }
} else {
item = { name: item, url: item }
}
@@ -139,30 +125,20 @@ export default {
return item
})
} else {
this.fileList = []
fileList.value = []
return []
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize)
},
},
methods: {
// 上传前loading加载
handleBeforeUpload(file) {
},{ deep: true, immediate: true })
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false
if (this.fileType.length) {
if (props.fileType.length) {
let fileExtension = ""
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
}
isImg = this.fileType.some(type => {
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
@@ -170,103 +146,113 @@ export default {
} else {
isImg = file.type.indexOf("image") > -1
}
if (!isImg) {
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}图片格式文件!`)
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
return false
}
if (file.name.includes(',')) {
this.$modal.msgError('文件名不正确,不能包含英文逗号!')
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`)
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false
}
}
this.$modal.loading("正在上传图片,请稍候...")
this.number++
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`)
},
// 上传成功回调
handleUploadSuccess(res, file) {
proxy.$modal.loading("正在上传图片,请稍候...")
number.value++
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName })
this.uploadedSuccessfully()
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
this.number--
this.$modal.closeLoading()
this.$modal.msgError(res.msg)
this.$refs.imageUpload.handleRemove(file)
this.uploadedSuccessfully()
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file)
uploadedSuccessfully()
}
},
// 删除图片
handleDelete(file) {
const findex = this.fileList.map(f => f.name).indexOf(file.name)
if (findex > -1) {
this.fileList.splice(findex, 1)
this.$emit("input", this.listToString(this.fileList))
}
// 删除图片
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
}
},
// 上传失败
handleUploadError() {
this.$modal.msgError("上传图片失败,请重试")
this.$modal.closeLoading()
},
// 上传结束处理
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()
}
// 上传结束处理
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()
}
},
// 预览
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
// 对象转成指定字符串分隔
listToString(list, separator) {
}
// 上传失败
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 (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : ''
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>
<style scoped lang="scss">
// .el-upload--picture-card 控制加号部分
::v-deep.hide .el-upload--picture-card {
:deep(.hide .el-upload--picture-card) {
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;
}
// 去掉动画效果
::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>
+28 -36
View File
@@ -1,26 +1,23 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<div :class="{ 'hidden': hidden }" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
<script setup>
import { scrollTo } from '@/utils/scroll-to'
export default {
name: 'Pagination',
props: {
const props = defineProps({
total: {
required: true,
type: Number
@@ -60,46 +57,41 @@ export default {
type: Boolean,
default: false
}
},
data() {
return {
}
},
computed: {
currentPage: {
})
const emit = defineEmits()
const currentPage = computed({
get() {
return this.page
return props.page
},
set(val) {
this.$emit('update:page', val)
emit('update:page', val)
}
},
pageSize: {
})
const pageSize = computed({
get() {
return this.limit
return props.limit
},
set(val) {
this.$emit('update:limit', val)
set(val){
emit('update:limit', val)
}
})
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1
}
},
methods: {
handleSizeChange(val) {
if (this.currentPage * val > this.total) {
this.currentPage = 1
}
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
emit('pagination', { page: currentPage.value, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
}
},
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
}
}
</script>
-141
View File
@@ -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>
+135 -139
View File
@@ -2,31 +2,33 @@
<div class="top-right-btn" :style="style">
<el-row>
<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 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 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-button size="mini" circle icon="el-icon-menu" />
<el-dropdown-menu slot="dropdown">
<el-button circle icon="Menu" />
<template #dropdown>
<el-dropdown-menu>
<!-- 全选/反选 按钮 -->
<el-dropdown-item>
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
</el-dropdown-item>
<div class="check-line"></div>
<template v-for="(item, key) in columns">
<el-dropdown-item :key="key">
<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-tooltip>
</el-row>
<el-dialog :title="title" :visible.sync="open" append-to-body>
<el-dialog :title="title" v-model="open" append-to-body>
<el-transfer
:titles="['显示', '隐藏']"
v-model="value"
@@ -37,22 +39,10 @@
</div>
</template>
<script>
<script setup>
import cache from '@/plugins/cache'
export default {
name: "RightToolbar",
data() {
return {
// 显隐数据
value: [],
// 弹出层标题
title: "显示/隐藏",
// 是否显示弹出层
open: false
}
},
props: {
const props = defineProps({
/* 是否显示检索条件 */
showSearch: {
type: Boolean,
@@ -83,80 +73,45 @@ export default {
type: String,
default: ""
}
},
computed: {
style() {
})
const emits = defineEmits(['update:showSearch', 'queryTable'])
// 显隐数据
const value = ref([])
// 弹出层标题
const title = ref("显示/隐藏")
// 是否显示弹出层
const open = ref(false)
const style = computed(() => {
const ret = {}
if (this.gutter) {
ret.marginRight = `${this.gutter / 2}px`
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`
}
return ret
},
isChecked: {
get() {
return Array.isArray(this.columns) ? this.columns.every((col) => col.visible) : Object.values(this.columns).every((col) => col.visible)
},
set() {}
},
isIndeterminate() {
return Array.isArray(this.columns) ? this.columns.some((col) => col.visible) && !this.isChecked : Object.values(this.columns).some((col) => col.visible) && !this.isChecked
},
transferData() {
if (Array.isArray(this.columns)) {
return this.columns.map((item, index) => ({ key: index, label: item.label }))
} else {
return Object.keys(this.columns).map((key, index) => ({ key: index, label: this.columns[key].label }))
}
}
},
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)
}
})
}
}
},
methods: {
// 搜索
toggleSearch() {
let el = this.$el
})
// 是否全选/半选 状态
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 this.$emit('update:showSearch', !this.showSearch)
this._animateSearch(formEl, this.showSearch)
},
// 搜索栏动画
_animateSearch(el, isHide) {
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: '' })
@@ -164,10 +119,10 @@ export default {
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)
setTimeout(() => { emits('update:showSearch', false); clear() }, DURATION)
} else {
this.$emit('update:showSearch', true)
this.$nextTick(() => {
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' })
@@ -175,75 +130,116 @@ export default {
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)
}
// 刷新
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(this.columns).forEach((key, index) => {
this.columns[key].visible = !data.includes(index)
Object.keys(props.columns).forEach((key, index) => {
props.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
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 {
this.columns[key].visible = event
Object.keys(props.columns).forEach(key => {
if (saved[key] !== undefined) props.columns[key].visible = saved[key]
})
}
}
} catch (e) {}
}
if (props.showColumnsType == "transfer") {
// transfer穿梭显隐列初始默认隐藏列
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item))
}
}
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))
Object.keys(props.columns).forEach((key, index) => {
if (props.columns[key].visible === false) {
value.value.push(index)
}
this.saveStorage()
},
// 将当前列显隐状态持久化到 localStorage
saveStorage() {
if (!this.storageKey) return
})
}
}
// 单勾选
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(this.columns)) {
this.columns.forEach((col, index) => { state[index] = col.visible })
if (Array.isArray(props.columns)) {
props.columns.forEach((col, index) => { state[index] = col.visible })
} else {
Object.keys(this.columns).forEach(key => { state[key] = this.columns[key].visible })
Object.keys(props.columns).forEach(key => { state[key] = props.columns[key].visible })
}
cache.local.setJSON(this.storageKey, state)
cache.local.setJSON(props.storageKey, state)
} catch (e) {}
}
},
}
</script>
<style lang="scss" scoped>
::v-deep .el-transfer__button {
<style lang='scss' scoped>
:deep(.el-transfer__button) {
border-radius: 50%;
padding: 12px;
display: block;
margin-left: 0px;
}
::v-deep .el-transfer__button:first-child {
:deep(.el-transfer__button:first-child) {
margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
line-height: 30px;
padding: 0 17px;
}
.check-line {
width: 90%;
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>
+6 -41
View File
@@ -1,55 +1,20 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div>
</template>
<script>
import screenfull from 'screenfull'
<script setup>
import { useFullscreen } from '@vueuse/core'
export default {
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)
}
}
}
}
const { isFullscreen, toggle } = useFullscreen()
</script>
<style scoped>
<style lang='scss' scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
fill: #5a5e66;
width: 20px;
height: 20px;
vertical-align: 10px;
+29 -41
View File
@@ -1,55 +1,43 @@
<template>
<el-dropdown trigger="click" @command="handleSetSize">
<div>
<el-dropdown trigger="click" @command="handleSetSize">
<div class="size-icon--style">
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
export default {
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)
<script setup>
import useAppStore from "@/store/modules/app"
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(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候...")
appStore.setSize(size)
setTimeout("window.location.reload()", 1000)
}
</script>
<style lang='scss' scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>
+26 -34
View File
@@ -1,15 +1,11 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
export default defineComponent({
props: {
iconClass: {
type: String,
@@ -18,44 +14,40 @@ export default {
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
color: {
type: String,
default: ''
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
setup(props) {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
iconName: computed(() => `#icon-${props.iconClass}`),
svgClass: computed(() => {
if (props.className) {
return `svg-icon ${props.className}`
}
return 'svg-icon'
})
}
}
}
}
})
</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 {
width: 1em;
height: 1em;
vertical-align: -0.15em;
position: relative;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
vertical-align: -2px;
}
</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>
+353 -306
View File
@@ -4,14 +4,17 @@
<div v-if="!collapsed" class="resize-handle" @mousedown="startResize" @touchstart="startResize" :class="{ active: isResizing }" />
<div class="tree-header">
<span class="tree-title" v-show="!collapsed">
<i :class="titleIconClass"></i> {{ title }}
<el-icon><component :is="titleIcon" /></el-icon> {{ title }}
</span>
<div class="tree-actions" v-show="!collapsed">
<el-tooltip :content="isExpandedAll ? '收起全部' : '展开全部'" placement="right">
<i class="tree-action-icon" :class="isExpandedAll ? 'el-icon-arrow-down' : 'el-icon-arrow-up'" @click="toggleExpandAll" />
<el-icon class="tree-action-icon" @click="toggleExpandAll">
<ArrowDown v-if="isExpandedAll" />
<ArrowUp v-else />
</el-icon>
</el-tooltip>
<el-tooltip content="刷新" placement="right">
<i class="tree-action-icon el-icon-refresh" @click="handleRefresh" />
<el-icon class="tree-action-icon" @click="handleRefresh"><Refresh /></el-icon>
</el-tooltip>
<slot name="actions"></slot>
</div>
@@ -20,12 +23,19 @@
<!-- 侧边栏展开/收起按钮 -->
<div class="collapse-button-container">
<el-tooltip :content="collapsed ? '展开' : '收起'" placement="right">
<i class="collapse-button" :class="collapsed ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'" @click="toggleCollapsed" />
<el-icon class="collapse-button" @click="toggleCollapsed">
<DArrowRight v-if="collapsed" />
<DArrowLeft v-else />
</el-icon>
</el-tooltip>
</div>
<div class="tree-search" v-show="!collapsed" v-if="showSearch">
<el-input v-model="searchKeyword" :placeholder="searchPlaceholder" clearable size="small" prefix-icon="el-icon-search" @input="onSearch" />
<el-input v-model="searchKeyword" :placeholder="searchPlaceholder" clearable>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="tree-wrap" v-show="!collapsed">
@@ -45,21 +55,24 @@
@node-expand="onNodeExpand"
@node-collapse="onNodeCollapse"
>
<span class="tree-node" slot-scope="{ node, data }">
<template #default="{ node, data }">
<slot name="node" :node="node" :data="data">
<i :class="data.children && data.children.length ? 'el-icon-folder' : 'el-icon-document'" class="node-icon" />
<span class="tree-node">
<el-icon class="node-icon">
<Folder v-if="data.children && data.children.length" />
<Document v-else />
</el-icon>
<span class="node-label" :title="node.label">{{ node.label }}</span>
</slot>
</span>
</slot>
</template>
</el-tree>
</div>
</div>
</template>
<script>
export default {
name: "TreeSidebar",
props: {
<script setup>
const props = defineProps({
// 树形数据
treeData: {
type: Array,
@@ -70,10 +83,10 @@ export default {
type: String,
default: '树形结构'
},
// 标题图标类名
titleIconClass: {
type: String,
default: 'el-icon-office-building'
// 标题图标
titleIcon: {
type: [String, Object],
default: 'OfficeBuilding'
},
// 是否显示搜索框
showSearch: {
@@ -163,337 +176,375 @@ export default {
type: Function,
default: null
}
},
data() {
return {
searchKeyword: "",
collapsed: this.defaultCollapsed,
sidebarWidth: this.defaultCollapsed ? this.collapsedWidth : this.defaultWidth,
isResizing: false,
startX: 0,
startWidth: 0,
saveWidthTimer: null,
rafId: null,
isLoadingFromStorage: false,
expandedAll: this.defaultExpandAll
};
},
computed: {
// 计算当前是否全部展开
isExpandedAll: {
get() {
return this.expandedAll;
},
set(val) {
this.expandedAll = val;
})
const emit = defineEmits([
'collapsed-change',
'expanded-all-change',
'refresh',
'node-click',
'check',
'node-expand',
'node-collapse',
'search'
])
const treeRef = ref(null)
// 响应式数据
const searchKeyword = ref('')
const collapsed = ref(props.defaultCollapsed)
const sidebarWidth = ref(props.defaultCollapsed ? props.collapsedWidth : props.defaultWidth)
const isResizing = ref(false)
const startX = ref(0)
const startWidth = ref(0)
const saveWidthTimer = ref(null)
const rafId = ref(null)
const isLoadingFromStorage = ref(false)
const expandedAll = ref(props.defaultExpandAll)
// 计算属性
const isExpandedAll = computed({
get: () => expandedAll.value,
set: (val) => {
expandedAll.value = val
}
})
// 节点过滤方法
const filterNodeMethod = (value, data) => {
if (props.filterMethod) {
return props.filterMethod(value, data)
}
},
watch: {
collapsed(newVal, oldVal) {
if (!value) return true
return data.label && data.label.indexOf(value) !== -1
}
// 监听折叠状态
watch(collapsed, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.handleCollapseChange(newVal);
this.$emit("collapsed-change", newVal);
handleCollapseChange(newVal)
emit('collapsed-change', newVal)
}
},
// 监听内部展开状态变化,触发实际树的展开/收起
expandedAll(newVal) {
this.$nextTick(() => {
})
// 监听内部展开状态变化,触发实际树的展开/收起
watch(expandedAll, (newVal) => {
nextTick(() => {
if (newVal) {
this.expandAllNodes();
expandAllNodes()
} else {
this.collapseAllNodes();
collapseAllNodes()
}
});
this.$emit("expanded-all-change", newVal);
},
// 监听搜索关键词
searchKeyword(val) {
if (this.$refs.treeRef) {
this.$refs.treeRef.filter(val);
this.$emit("search", val);
}
}
},
mounted() {
this.isLoadingFromStorage = true
if (!this.collapsed && this.enableStorage) {
const savedWidth = this.getSavedWidth();
if (savedWidth !== null) {
this.sidebarWidth = savedWidth;
}
}
this.$nextTick(() => {
this.isLoadingFromStorage = false
})
if (this.expandedAll) {
this.$nextTick(() => {
this.expandAllNodes();
});
emit('expanded-all-change', newVal)
})
// 监听搜索关键词
watch(searchKeyword, (val) => {
if (treeRef.value) {
treeRef.value.filter(val)
emit('search', val)
}
},
beforeDestroy() {
this.cleanup();
},
methods: {
// 节点过滤方法
filterNodeMethod(value, data) {
if (this.filterMethod) {
return this.filterMethod(value, data);
})
// 清理定时器和动画帧
const cleanup = () => {
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
}
if (!value) return true;
return data.label && data.label.indexOf(value) !== -1;
},
// 清理定时器和动画帧
cleanup() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
if (saveWidthTimer.value) {
clearTimeout(saveWidthTimer.value)
saveWidthTimer.value = null
}
if (this.saveWidthTimer) {
clearTimeout(this.saveWidthTimer);
this.saveWidthTimer = null;
}
},
// 处理收起/展开状态变化
handleCollapseChange(isCollapsed) {
}
// 处理收起/展开状态变化
const handleCollapseChange = (isCollapsed) => {
if (isCollapsed) {
this.saveWidthToStorage();
this.sidebarWidth = this.collapsedWidth;
saveWidthToStorage()
sidebarWidth.value = props.collapsedWidth
} else {
const savedWidth = this.getSavedWidth();
this.sidebarWidth = savedWidth !== null ? savedWidth : this.defaultWidth;
const savedWidth = getSavedWidth()
sidebarWidth.value = savedWidth !== null ? savedWidth : props.defaultWidth
}
},
// 获取保存的宽度
getSavedWidth() {
if (!this.enableStorage) {
return null;
}
// 获取保存的宽度
const getSavedWidth = () => {
if (!props.enableStorage) {
return null
}
try {
const savedWidth = localStorage.getItem(this.storageKey);
const savedWidth = localStorage.getItem(props.storageKey)
if (savedWidth) {
const width = parseInt(savedWidth, 10);
if (!isNaN(width) && width >= this.minWidth && width <= this.maxWidth) {
return width;
const width = parseInt(savedWidth, 10)
if (!isNaN(width) && width >= props.minWidth && width <= props.maxWidth) {
return width
}
}
} catch (error) {
console.warn(`Failed to load sidebar width from storage with key ${this.storageKey}:`, error);
console.warn(`Failed to load sidebar width from storage with key ${props.storageKey}:`, error)
}
return null;
},
// 保存宽度到本地存储
saveWidthToStorage() {
if (this.collapsed || !this.enableStorage) return;
return null
}
// 保存宽度到本地存储
const saveWidthToStorage = () => {
if (collapsed.value || !props.enableStorage) return
try {
localStorage.setItem(this.storageKey, this.sidebarWidth.toString());
localStorage.setItem(props.storageKey, sidebarWidth.value.toString())
} catch (error) {
console.warn(`Failed to save sidebar width to storage with key ${this.storageKey}:`, error);
console.warn(`Failed to save sidebar width to storage with key ${props.storageKey}:`, error)
}
},
// 切换侧边栏收起/展开状态
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
// 切换展开/折叠所有节点
toggleExpandAll() {
this.isExpandedAll = !this.isExpandedAll;
},
// 展开所有节点
expandAllNodes() {
if (!this.$refs.treeRef) return;
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
}
// 切换侧边栏收起/展开状态
const toggleCollapsed = () => {
collapsed.value = !collapsed.value
}
// 切换展开/折叠所有节点
const toggleExpandAll = () => {
expandedAll.value = !expandedAll.value
}
// 展开所有节点
const expandAllNodes = () => {
if (!treeRef.value) return
const allNodes = getAllNodes(treeRef.value.root)
allNodes.forEach(node => {
if (node.expanded !== undefined && !node.expanded) {
node.expanded = true;
node.expanded = true
}
});
},
// 获取所有节点
getAllNodes(rootNode) {
const nodes = [];
})
}
// 获取所有节点
const getAllNodes = (rootNode) => {
const nodes = []
const traverse = (node) => {
if (!node) return;
nodes.push(node);
if (!node) return
nodes.push(node)
if (node.childNodes && node.childNodes.length) {
node.childNodes.forEach(child => traverse(child));
node.childNodes.forEach(child => traverse(child))
}
};
traverse(rootNode);
return nodes;
},
// 收起所有节点
collapseAllNodes() {
if (!this.$refs.treeRef) return;
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
}
traverse(rootNode)
return nodes
}
// 收起所有节点
const collapseAllNodes = () => {
if (!treeRef.value) return
const allNodes = getAllNodes(treeRef.value.root)
allNodes.forEach(node => {
if (node.expanded !== undefined && node.expanded) {
node.expanded = false;
node.expanded = false
}
});
},
// 处理刷新操作
handleRefresh() {
this.$emit("refresh");
},
// 节点点击事件
onNodeClick(data, node, e) {
this.$emit("node-click", data, node, e);
},
// 复选框选中事件
onCheck(data, checkedInfo) {
this.$emit("check", data, checkedInfo);
},
// 节点展开事件
onNodeExpand(data, node, e) {
this.$emit("node-expand", data, node, e);
},
// 节点折叠事件
onNodeCollapse(data, node, e) {
this.$emit("node-collapse", data, node, e);
},
// 搜索处理
onSearch() {
// 搜索逻辑已在 watch 中处理
},
// 设置当前选中的节点
setCurrentKey(key) {
if (this.$refs.treeRef) {
this.$refs.treeRef.setCurrentKey(key);
})
}
// 处理刷新操作
const handleRefresh = () => {
emit('refresh')
}
// 节点点击事件
const onNodeClick = (data, node, e) => {
emit('node-click', data, node, e)
}
// 复选框选中事件
const onCheck = (data, checkedInfo) => {
emit('check', data, checkedInfo)
}
// 节点展开事件
const onNodeExpand = (data, node, e) => {
emit('node-expand', data, node, e)
}
// 节点折叠事件
const onNodeCollapse = (data, node, e) => {
emit('node-collapse', data, node, e)
}
const setCurrentKey = (key) => {
if (treeRef.value) {
treeRef.value.setCurrentKey(key)
}
},
// 获取当前选中的节点
getCurrentNode() {
if (this.$refs.treeRef) {
return this.$refs.treeRef.getCurrentNode();
}
const getCurrentNode = () => {
if (treeRef.value) {
return treeRef.value.getCurrentNode()
}
return null;
},
// 获取当前选中的节点的key
getCurrentKey() {
if (this.$refs.treeRef) {
return this.$refs.treeRef.getCurrentKey();
return null
}
const getCurrentKey = () => {
if (treeRef.value) {
return treeRef.value.getCurrentKey()
}
return null;
},
// 设置选中的节点keys(复选框)
setCheckedKeys(keys) {
if (this.$refs.treeRef && this.showCheckbox) {
this.$refs.treeRef.setCheckedKeys(keys);
return null
}
const setCheckedKeys = (keys) => {
if (treeRef.value && props.showCheckbox) {
treeRef.value.setCheckedKeys(keys)
}
},
// 获取选中的节点keys(复选框)
getCheckedKeys() {
if (this.$refs.treeRef && this.showCheckbox) {
return this.$refs.treeRef.getCheckedKeys();
}
const getCheckedKeys = () => {
if (treeRef.value && props.showCheckbox) {
return treeRef.value.getCheckedKeys()
}
return [];
},
// 获取选中的节点(复选框)
getCheckedNodes() {
if (this.$refs.treeRef && this.showCheckbox) {
return this.$refs.treeRef.getCheckedNodes();
return []
}
const getCheckedNodes = () => {
if (treeRef.value && props.showCheckbox) {
return treeRef.value.getCheckedNodes()
}
return [];
},
// 清空搜索
clearSearch() {
this.searchKeyword = "";
if (this.$refs.treeRef) {
this.$refs.treeRef.filter("");
return []
}
const clearSearch = () => {
searchKeyword.value = ""
if (treeRef.value) {
treeRef.value.filter("")
}
},
// 过滤树
filter(value) {
this.searchKeyword = value;
},
// 开始调整大小
startResize(e) {
e.preventDefault();
e.stopPropagation();
this.isResizing = true;
this.startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
this.startWidth = this.sidebarWidth;
}
const filter = (value) => {
searchKeyword.value = value
}
const startResize = (e) => {
e.preventDefault()
e.stopPropagation()
isResizing.value = true
startX.value = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX
startWidth.value = sidebarWidth.value
if (e.type === 'mousedown') {
document.addEventListener('mousemove', this.handleResizeMove);
document.addEventListener('mouseup', this.stopResize);
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', stopResize)
} else {
document.addEventListener('touchmove', this.handleResizeMove, { passive: false });
document.addEventListener('touchend', this.stopResize);
document.addEventListener('touchmove', handleResizeMove, { passive: false })
document.addEventListener('touchend', stopResize)
}
this.disableUserSelect();
},
// 处理调整大小移动
handleResizeMove(e) {
if (!this.isResizing) return;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
disableUserSelect()
}
const handleResizeMove = (e) => {
if (!isResizing.value) return
if (rafId.value) {
cancelAnimationFrame(rafId.value)
}
this.rafId = requestAnimationFrame(() => {
e.preventDefault();
e.stopPropagation();
const clientX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
const deltaX = clientX - this.startX;
const newWidth = this.startWidth + deltaX;
const clampedWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
if (Math.abs(clampedWidth - this.sidebarWidth) >= 1) {
this.sidebarWidth = clampedWidth;
rafId.value = requestAnimationFrame(() => {
e.preventDefault()
e.stopPropagation()
const clientX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX
const deltaX = clientX - startX.value
const newWidth = startWidth.value + deltaX
const clampedWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth))
if (Math.abs(clampedWidth - sidebarWidth.value) >= 1) {
sidebarWidth.value = clampedWidth
}
});
},
// 停止调整大小
stopResize() {
if (!this.isResizing) return;
this.isResizing = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
})
}
const stopResize = () => {
if (!isResizing.value) return
isResizing.value = false
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
}
this.startX = 0;
this.startWidth = 0;
document.removeEventListener('mousemove', this.handleResizeMove);
document.removeEventListener('mouseup', this.stopResize);
document.removeEventListener('touchmove', this.handleResizeMove);
document.removeEventListener('touchend', this.stopResize);
this.enableUserSelect();
this.saveWidthToStorage();
},
// 禁用用户选择
disableUserSelect() {
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
document.body.style.mozUserSelect = 'none';
document.body.style.msUserSelect = 'none';
},
// 启用用户选择
enableUserSelect() {
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
document.body.style.mozUserSelect = '';
document.body.style.msUserSelect = '';
},
// 重置宽度到默认值
resetWidth() {
this.sidebarWidth = this.defaultWidth;
this.saveWidthToStorage();
},
// 获取当前宽度
getCurrentWidth() {
return this.sidebarWidth;
},
// 设置宽度
setWidth(width) {
if (typeof width === 'number' && width >= this.minWidth && width <= this.maxWidth) {
this.sidebarWidth = width;
if (!this.collapsed) {
this.saveWidthToStorage();
startX.value = 0
startWidth.value = 0
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', stopResize)
document.removeEventListener('touchmove', handleResizeMove)
document.removeEventListener('touchend', stopResize)
enableUserSelect()
saveWidthToStorage()
}
const disableUserSelect = () => {
document.body.style.userSelect = 'none'
document.body.style.webkitUserSelect = 'none'
document.body.style.mozUserSelect = 'none'
document.body.style.msUserSelect = 'none'
}
const enableUserSelect = () => {
document.body.style.userSelect = ''
document.body.style.webkitUserSelect = ''
document.body.style.mozUserSelect = ''
document.body.style.msUserSelect = ''
}
const resetWidth = () => {
sidebarWidth.value = props.defaultWidth
saveWidthToStorage()
}
const getCurrentWidth = () => {
return sidebarWidth.value
}
const setWidth = (width) => {
if (typeof width === 'number' && width >= props.minWidth && width <= props.maxWidth) {
sidebarWidth.value = width
if (!collapsed.value) {
saveWidthToStorage()
}
}
}
defineExpose({
setCurrentKey,
getCurrentNode,
getCurrentKey,
setCheckedKeys,
getCheckedKeys,
getCheckedNodes,
clearSearch,
filter,
resetWidth,
getCurrentWidth,
setWidth,
expandAllNodes,
collapseAllNodes,
toggleCollapsed,
treeRef
})
onMounted(() => {
isLoadingFromStorage.value = true
if (!collapsed.value && props.enableStorage) {
const savedWidth = getSavedWidth()
if (savedWidth !== null) {
sidebarWidth.value = savedWidth
}
}
};
nextTick(() => {
isLoadingFromStorage.value = false
})
if (expandedAll.value) {
nextTick(() => {
expandAllNodes()
})
}
})
onBeforeUnmount(() => {
cleanup()
})
</script>
<style lang="scss" scoped>
@@ -574,7 +625,7 @@ export default {
}
.collapse-button {
font-size: 14px;
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 4px;
@@ -607,9 +658,9 @@ export default {
align-items: center;
gap: 5px;
i {
.el-icon {
color: #409eff;
font-size: 14px;
font-size: 16px;
}
}
@@ -622,7 +673,7 @@ export default {
}
.tree-action-icon {
font-size: 14px;
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 4px;
@@ -662,7 +713,7 @@ export default {
}
}
::v-deep .el-tree-node__content {
:deep(.el-tree-node__content) {
height: 32px;
border-radius: 4px;
margin-bottom: 1px;
@@ -672,7 +723,7 @@ export default {
}
}
::v-deep .el-tree-node.is-current > .el-tree-node__content {
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #e6f0fd;
color: #409eff;
font-weight: 600;
@@ -702,8 +753,4 @@ export default {
white-space: nowrap;
}
}
::v-deep .el-icon-document.node-icon {
color: #909399 !important;
}
</style>
+15 -20
View File
@@ -1,36 +1,31 @@
<template>
<div v-loading="loading" :style="'height:' + height">
<iframe
:src="src"
:src="url"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
/>
scrolling="auto" />
</div>
</template>
<script>
export default {
props: {
<script setup>
const props = defineProps({
src: {
type: String,
required: true
},
},
data() {
return {
height: document.documentElement.clientHeight - 94.5 + "px;",
loading: true,
url: this.src
}
},
mounted: function () {
})
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
const loading = ref(true)
const url = computed(() => props.src)
onMounted(() => {
setTimeout(() => {
this.loading = false
loading.value = false
}, 300)
const that = this
window.onresize = function temp() {
that.height = document.documentElement.clientHeight - 94.5 + "px;"
height.value = document.documentElement.clientHeight - 94.5 + "px;"
}
}
}
})
</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
}
-64
View File
@@ -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)
}
}
+5 -19
View File
@@ -1,23 +1,9 @@
import hasRole from './permission/hasRole'
import hasPermi from './permission/hasPermi'
import dialogDrag from './dialog/drag'
import dialogDragWidth from './dialog/dragWidth'
import dialogDragHeight from './dialog/dragHeight'
import clipboard from './module/clipboard'
import copyText from './common/copyText'
const install = function(Vue) {
Vue.directive('hasRole', hasRole)
Vue.directive('hasPermi', hasPermi)
Vue.directive('clipboard', clipboard)
Vue.directive('dialogDrag', dialogDrag)
Vue.directive('dialogDragWidth', dialogDragWidth)
Vue.directive('dialogDragHeight', dialogDragHeight)
export default function directive(app){
app.directive('hasRole', hasRole)
app.directive('hasPermi', hasPermi)
app.directive('copyText', copyText)
}
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 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import store from '@/store'
import useUserStore from '@/store/modules/user'
export default {
inserted(el, binding, vnode) {
mounted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*"
const permissions = store.getters && store.getters.permissions
const permissions = useUserStore().permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
@@ -2,14 +2,13 @@
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
import store from '@/store'
import useUserStore from '@/store/modules/user'
export default {
inserted(el, binding, vnode) {
mounted(el, binding, vnode) {
const { value } = binding
const super_admin = "admin"
const roles = store.getters && store.getters.roles
const roles = useUserStore().roles
if (value && value instanceof Array && value.length > 0) {
const roleFlag = value
@@ -22,7 +21,7 @@ export default {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置角色权限标签值"`)
throw new Error(`请设置角色权限标签值`)
}
}
}
+20 -37
View File
@@ -1,45 +1,36 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view v-if="!$route.meta.link" :key="key" />
<keep-alive :include="tagsViewStore.cachedViews">
<component v-if="!route.meta.link" :is="Component" :key="route.path"/>
</keep-alive>
</transition>
</router-view>
<iframe-toggle />
<copyright />
</section>
</template>
<script>
<script setup>
import copyright from "./Copyright/index"
import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView'
export default {
name: 'AppMain',
components: { iframeToggle, copyright },
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
},
watch: {
$route() {
this.addIframe()
}
},
mounted() {
this.addIframe()
},
methods: {
addIframe() {
const { name } = this.$route
if (name && this.$route.meta.link) {
this.$store.dispatch('tagsView/addIframeView', this.$route)
}
}
const route = useRoute()
const tagsViewStore = useTagsViewStore()
onMounted(() => {
addIframe()
})
watchEffect(() => {
addIframe()
})
function addIframe() {
if (route.meta.link) {
useTagsViewStore().addIframeView(route)
}
}
</script>
@@ -51,14 +42,6 @@ export default {
width: 100%;
position: relative;
overflow: hidden;
&:fullscreen,
&:-webkit-full-screen,
&:-moz-full-screen,
&:-ms-fullscreen {
background: #fff;
overflow-y: auto;
}
}
.fixed-header + .app-main {
@@ -4,17 +4,13 @@
</footer>
</template>
<script>
export default {
computed: {
visible() {
return this.$store.state.settings.footerVisible
},
content() {
return this.$store.state.settings.footerContent
}
}
}
<script setup>
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const visible = computed(() => settingsStore.footerVisible)
const content = computed(() => settingsStore.footerContent)
</script>
<style scoped>
@@ -1,20 +1,20 @@
<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-if="!detail" class="notice-empty">
<i class="el-icon-document"></i>
<el-icon><Document /></el-icon>
<span>暂无数据</span>
</div>
<div v-else class="notice-page">
<div class="notice-type-wrap">
<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 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 v-else class="notice-type-tag type-notify">
<i class="el-icon-document"></i> 消息
<el-icon><Document /></el-icon> 消息
</span>
</div>
@@ -22,11 +22,11 @@
<div class="notice-meta">
<span class="meta-item">
<i class="el-icon-user"></i>
<el-icon><User /></el-icon>
<span>{{ detail.createBy || '—' }}</span>
</span>
<span class="meta-item">
<i class="el-icon-time"></i>
<el-icon><Clock /></el-icon>
<span>{{ detail.createTime || '—' }}</span>
</span>
<span class="meta-item">
@@ -44,7 +44,7 @@
<div class="notice-body">
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
<div v-else class="notice-empty notice-empty--inner">
<i class="el-icon-document"></i> 暂无内容
<el-icon><Document /></el-icon> 暂无内容
</div>
</div>
</div>
@@ -52,30 +52,24 @@
</el-drawer>
</template>
<script>
<script setup>
import { getNotice } from '@/api/system/notice'
export default {
name: 'NoticeDetailView',
data() {
return {
visible: false,
loading: false,
detail: null
}
},
computed: {
isStatusNormal() {
const s = this.detail && this.detail.status
return s === '0' || s === 0
},
hasContent() {
const c = this.detail && this.detail.noticeContent
return c != null && String(c).trim() !== ''
}
},
methods: {
open(payload) {
const visible = ref(false)
const loading = ref(false)
const detail = ref(null)
const isStatusNormal = computed(() => {
const status = detail.value && detail.value.status
return status === '0' || status === 0
})
const hasContent = computed(() => {
const content = detail.value && detail.value.noticeContent
return content != null && String(content).trim() !== ''
})
function open(payload) {
let id = null
let preset = null
if (payload != null && typeof payload === 'object') {
@@ -86,32 +80,35 @@ export default {
} else {
id = payload
}
this.visible = true
visible.value = true
if (preset) {
this.detail = preset
detail.value = preset
return
}
if (id == null || id === '') {
this.detail = null
detail.value = null
return
}
this.loading = true
this.detail = null
loading.value = true
detail.value = null
getNotice(id).then(res => {
this.detail = res.data
detail.value = res.data
}).catch(() => {
this.detail = null
detail.value = null
}).finally(() => {
this.loading = false
loading.value = false
})
},
handleClose() {
this.visible = false
this.detail = null
this.loading = false
}
}
}
function handleClose() {
visible.value = false
detail.value = null
loading.value = false
}
defineExpose({
open
})
</script>
<style lang="scss" scoped>
@@ -186,7 +183,7 @@ export default {
color: #718096;
}
.meta-item i {
.meta-item .el-icon {
font-size: 12px;
color: #a0aec0;
}
@@ -244,56 +241,56 @@ export default {
word-break: break-word;
}
.notice-content ::v-deep p {
.notice-content :deep(p) {
margin: 0 0 1em;
}
.notice-content ::v-deep h1,
.notice-content ::v-deep h2,
.notice-content ::v-deep h3 {
.notice-content :deep(h1),
.notice-content :deep(h2),
.notice-content :deep(h3) {
font-weight: 700;
color: #1a202c;
margin: 1.4em 0 0.6em;
}
.notice-content ::v-deep h1 {
.notice-content :deep(h1) {
font-size: 18px;
}
.notice-content ::v-deep h2 {
.notice-content :deep(h2) {
font-size: 16px;
}
.notice-content ::v-deep h3 {
.notice-content :deep(h3) {
font-size: 14px;
}
.notice-content ::v-deep a {
.notice-content :deep(a) {
color: #3182ce;
text-decoration: underline;
}
.notice-content ::v-deep a:hover {
.notice-content :deep(a:hover) {
color: #2b6cb0;
}
.notice-content ::v-deep img {
.notice-content :deep(img) {
max-width: 100%;
border-radius: 4px;
margin: 8px 0;
}
.notice-content ::v-deep ul,
.notice-content ::v-deep ol {
.notice-content :deep(ul),
.notice-content :deep(ol) {
padding-left: 20px;
margin: 0 0 1em;
}
.notice-content ::v-deep li {
.notice-content :deep(li) {
margin-bottom: 4px;
}
.notice-content ::v-deep blockquote {
.notice-content :deep(blockquote) {
border-left: 3px solid #cbd5e0;
margin: 1em 0;
padding: 6px 16px;
@@ -301,20 +298,20 @@ export default {
background: #f7fafc;
}
.notice-content ::v-deep table {
.notice-content :deep(table) {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 13px;
}
.notice-content ::v-deep table th,
.notice-content ::v-deep table td {
.notice-content :deep(table th),
.notice-content :deep(table td) {
border: 1px solid #e2e8f0;
padding: 7px 12px;
}
.notice-content ::v-deep table th {
.notice-content :deep(table th) {
background: #f7fafc;
font-weight: 600;
}
@@ -326,9 +323,9 @@ export default {
font-size: 13px;
}
.notice-empty i {
.notice-empty .el-icon {
font-size: 28px;
display: block;
display: inline-flex;
margin-bottom: 10px;
}
@@ -336,27 +333,27 @@ export default {
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 {
height: 100%;
overflow: auto;
padding: 10px 16px 22px;
}
</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>
<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">
<span class="notice-title">通知公告</span>
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
</div>
<div v-if="noticeLoading" class="notice-loading"><i class="el-icon-loading"></i> 加载中...</div>
<div v-else-if="noticeList.length === 0" class="notice-empty"><i class="el-icon-inbox"></i><br>暂无公告</div>
<div v-if="noticeLoading" class="notice-loading">
<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-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' ? '通知' : '公告' }}
</el-tag>
<span class="notice-item-title">{{ item.noticeTitle }}</span>
<span class="notice-item-date">{{ item.createTime }}</span>
</div>
</div>
</el-popover>
<div v-popover:noticePopover class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
<!-- 触发器 -->
<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>
<!-- 预览弹窗 -->
<notice-detail-view ref="noticeViewRef" />
</div>
</template>
<script>
<script setup>
import NoticeDetailView from './DetailView'
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
export default {
name: 'HeaderNotice',
components: { NoticeDetailView },
data() {
return {
noticeList: [], // 通知列表
unreadCount: 0, // 未读数量
noticeLoading: false, // 加载状态
noticeVisible: false, // 弹出层显示状态
noticeLeaveTimer: null // 鼠标离开计时器
}
},
mounted() {
this.loadNoticeTop()
},
methods: {
// 鼠标移入铃铛区域
onNoticeEnter() {
clearTimeout(this.noticeLeaveTimer)
this.noticeVisible = true
this.$nextTick(() => {
const popper = this.$refs.noticePopover.$refs.popper
const noticePopover = ref(null)
const noticeList = ref([])
const unreadCount = ref(0)
const noticeLoading = ref(false)
const noticeVisible = ref(false)
const noticeLeaveTimer = ref(null)
const { proxy } = getCurrentInstance()
// 加载顶部公告列表
function loadNoticeTop() {
noticeLoading.value = true
listNoticeTop().then(res => {
noticeList.value = res.data || []
unreadCount.value = res.unreadCount !== undefined ? res.unreadCount : noticeList.value.filter(n => !n.isRead).length
}).finally(() => {
noticeLoading.value = false
})
}
onMounted(() => loadNoticeTop())
// 鼠标移入铃铛区域
function onNoticeEnter() {
clearTimeout(noticeLeaveTimer.value)
noticeVisible.value = true
nextTick(() => {
const popper = noticePopover.value?.popperRef?.contentRef
if (popper && !popper._noticeBound) {
popper._noticeBound = true
popper.addEventListener('mouseenter', () => clearTimeout(this.noticeLeaveTimer))
popper.addEventListener('mouseenter', () => clearTimeout(noticeLeaveTimer.value))
popper.addEventListener('mouseleave', () => {
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 100)
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) {
}
// 鼠标离开铃铛区域
function onNoticeLeave() {
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 150)
}
// 预览公告详情
function 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)
const idx = noticeList.value.indexOf(item)
if (idx !== -1) noticeList.value[idx] = { ...item, isRead: true }
unreadCount.value = Math.max(0, unreadCount.value - 1)
}
this.$refs.noticeViewRef.open(item.noticeId)
},
// 全部已读
markAllRead() {
const ids = this.noticeList.map(n => n.noticeId).join(',')
proxy.$refs["noticeViewRef"].open(item.noticeId)
}
// 全部已读
function markAllRead() {
const ids = noticeList.value.map(n => n.noticeId).join(',')
if (!ids) return
markNoticeReadAll(ids).catch(() => {})
this.noticeList = this.noticeList.map(n => ({ ...n, isRead: true }))
this.unreadCount = 0
}
}
noticeList.value = noticeList.value.map(n => ({ ...n, isRead: true }))
unreadCount.value = 0
}
</script>
@@ -121,9 +126,7 @@ export default {
pointer-events: none;
}
}
.notice-popover {
padding: 0 !important;
}
.notice-popover { padding: 0 !important; }
.notice-popover .notice-header {
display: flex;
align-items: center;
@@ -137,7 +140,7 @@ export default {
}
.notice-popover .notice-mark-all {
font-size: 12px;
color: #409EFF;
color: var(--el-color-primary);
font-weight: normal;
cursor: pointer;
}
@@ -1,33 +1,25 @@
<template>
<transition-group name="fade-transform" mode="out-in">
<inner-link
v-for="(item, index) in iframeViews"
v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path"
:iframeId="'iframe' + index"
v-show="$route.path === item.path"
v-show="route.path === item.path"
:src="iframeUrl(item.meta.link, item.query)"
></inner-link>
</transition-group>
</template>
<script>
<script setup>
import InnerLink from "../InnerLink/index"
import useTagsViewStore from "@/store/modules/tagsView"
export default {
components: { InnerLink },
computed: {
iframeViews() {
return this.$store.state.tagsView.iframeViews
}
},
methods: {
iframeUrl(url, query) {
const route = useRoute()
const tagsViewStore = useTagsViewStore()
function 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
}
}
}
</script>
@@ -4,14 +4,14 @@
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
ref="iframeRef"
frameborder="no"
></iframe>
</div>
</template>
<script>
export default {
props: {
<script setup>
const props = defineProps({
src: {
type: String,
default: "/"
@@ -19,29 +19,17 @@ export default {
iframeId: {
type: String
}
},
data() {
return {
loading: false,
height: document.documentElement.clientHeight - 94.5 + "px;"
}
},
mounted() {
var _this = this
const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/")
const iframe = document.querySelector(iframeId)
// iframe页面loading控制
if (iframe.attachEvent) {
this.loading = true
iframe.attachEvent("onload", function () {
_this.loading = false
})
} else {
this.loading = true
iframe.onload = function () {
_this.loading = false
})
const loading = ref(true)
const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
const iframeRef = ref(null)
onMounted(() => {
if (iframeRef.value) {
iframeRef.value.onload = () => {
loading.value = false
}
}
}
}
})
</script>
+135 -83
View File
@@ -1,16 +1,16 @@
<template>
<div class="navbar" :class="'nav' + navType">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb v-if="navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="navType == 2" id="topmenu-container" class="topmenu-container" />
<template v-if="navType == 3">
<logo v-show="showLogo" :collapse="false"></logo>
<div class="navbar" :class="'nav' + settingsStore.navType">
<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" />
<top-nav v-if="settingsStore.navType == 2" id="topmenu-container" class="topmenu-container" />
<template v-if="settingsStore.navType == 3">
<logo v-show="settingsStore.sidebarLogo" :collapse="false"></logo>
<top-bar id="topbar-container" class="topbar-container" />
</template>
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<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" />
<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">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
@@ -29,35 +36,36 @@
<el-tooltip content="消息通知" effect="dark" placement="bottom">
<header-notice id="header-notice" class="right-menu-item hover-effect" />
</el-tooltip>
</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">
<img :src="avatar" class="user-avatar">
<span class="user-nickname"> {{ nickName }} </span>
<img :src="userStore.avatar" class="user-avatar" />
<span class="user-nickname"> {{ userStore.nickName }} </span>
</div>
<el-dropdown-menu slot="dropdown">
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setLayout" v-if="setting">
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item @click.native="lockScreen">
<el-dropdown-item command="lockScreen">
<span>锁定屏幕</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
<script setup>
import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from './TopNav'
import TopBar from './TopBar'
@@ -65,77 +73,107 @@ import Logo from './Sidebar/Logo'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
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'
export default {
components: {
Breadcrumb,
Logo,
TopNav,
TopBar,
Hamburger,
Screenfull,
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc,
HeaderNotice
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device',
'nickName'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const lockStore = useLockStore()
const settingsStore = useSettingsStore()
function toggleSideBar() {
appStore.toggleSideBar()
}
function handleCommand(command) {
switch (command) {
case "setLayout":
setLayout()
break
case "lockScreen":
lockScreen()
break
case "logout":
logout()
break
default:
break
}
},
navType: {
get() {
return this.$store.state.settings.navType
}
},
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('确定注销并退出系统吗?', '提示', {
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
userStore.logOut().then(() => {
location.href = '/index'
})
}).catch(() => {})
}).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)"
}
)
await transition.finished
} catch (error) {
console.warn("View transition failed, falling back to immediate toggle:", error)
settingsStore.toggleTheme()
}
}
</script>
<style lang="scss" scoped>
<style lang='scss' scoped>
.navbar.nav3 {
.hamburger-container {
display: none !important;
@@ -146,8 +184,8 @@ export default {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
background: var(--navbar-bg);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
// padding: 0 8px;
@@ -157,15 +195,15 @@ export default {
line-height: 46px;
height: 100%;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
&:hover {
background: rgba(0, 0, 0, .025)
background: rgba(0, 0, 0, 0.025);
}
}
@@ -208,10 +246,23 @@ export default {
&.hover-effect {
cursor: pointer;
transition: background .3s;
transition: background 0.3s;
&: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;
width: 30px;
height: 30px;
margin-right: 8px;
border-radius: 50%;
}
.user-nickname{
position: relative;
left: 0px;
bottom: 10px;
left: 2px;
font-size: 14px;
font-weight: bold;
}
.el-icon-caret-bottom {
i {
cursor: pointer;
position: absolute;
right: -20px;
+154 -240
View File
@@ -1,25 +1,22 @@
<template>
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
<div class="drawer-container">
<div>
<div class="setting-drawer-content">
<el-drawer v-model="showSettings" :withHeader="false" :lock-scroll="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">菜单导航设置</h3>
</div>
<div class="nav-wrap">
<el-tooltip content="左侧菜单" placement="bottom">
<div class="item left" @click="handleNavType(1)" :style="{'--theme': theme}" :class="{ activeItem: navType == 1 }">
<div class="item left" @click="handleNavType(1)" :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 }">
<div class="item mix" @click="handleNavType(2)" :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 }">
<div class="item top" @click="handleNavType(3)" :class="{ activeItem: navType == 3 }">
<b></b><b></b>
</div>
</el-tooltip>
@@ -29,285 +26,216 @@
</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">
<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 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">
<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 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" />
<span class="comp-style">
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
</span>
</div>
</div>
<el-divider/>
<el-divider />
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启页签</span>
<el-switch v-model="tagsView" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>持久化标签页</span>
<el-switch v-model="tagsViewPersist" :disabled="!tagsView" class="drawer-switch" />
<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>
<el-switch v-model="tagsIcon" :disabled="!tagsView" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>标签页样式</span>
<el-radio-group v-model="tagsViewStyle" :disabled="!tagsView" size="mini" class="drawer-switch">
<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>
<el-switch v-model="fixedHeader" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<el-switch v-model="dynamicTitle" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" @change="dynamicTitleChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>底部版权</span>
<el-switch v-model="footerVisible" class="drawer-switch" />
<span class="comp-style">
<el-switch v-model="settingsStore.footerVisible" class="drawer-switch" />
</span>
</div>
<el-divider/>
<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>
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
</el-drawer>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
<script setup>
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 {
components: { ThemePicker },
expose: ['openSetting'],
data() {
return {
theme: this.$store.state.settings.theme,
sideTheme: this.$store.state.settings.sideTheme,
navType: this.$store.state.settings.navType,
showSettings: false
}
},
computed: {
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
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)
}
}
const { proxy } = getCurrentInstance()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const showSettings = ref(false)
const navType = ref(settingsStore.navType)
const theme = ref(settingsStore.theme)
const sideTheme = ref(settingsStore.sideTheme)
const tagsViewPersist = ref(settingsStore.tagsViewPersist)
const storeSettings = computed(() => settingsStore)
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"])
/** 是否需要dynamicTitle */
function dynamicTitleChange() {
useSettingsStore().setTitle(useSettingsStore().title)
}
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>
<style lang="scss" scoped>
.setting-drawer-content {
.setting-drawer-title {
<style lang='scss' scoped>
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
line-height: 22px;
font-weight: bold;
}
.setting-drawer-block-checbox {
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
@@ -338,30 +266,16 @@ export default {
font-size: 14px;
}
}
}
}
.drawer-container {
padding: 20px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
.drawer-item {
color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
padding: 12px 0;
}
font-size: 14px;
.drawer-switch {
float: right
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
}
}
@@ -374,7 +288,7 @@ export default {
margin-bottom: 20px;
.activeItem {
border: 2px solid #{'var(--theme)'} !important;
border: 2px solid var(--el-color-primary) !important;
}
.item {
@@ -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>
<component :is="type" v-bind="linkProps(to)">
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>
<script>
<script setup>
import { isExternal } from '@/utils/validate'
export default {
props: {
const props = defineProps({
to: {
type: [String, Object],
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
})
const isExt = computed(() => {
return isExternal(props.to)
})
const type = computed(() => {
if (isExt.value) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
})
function linkProps() {
if (isExt.value) {
return {
href: to,
href: props.to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
to: props.to
}
}
</script>
@@ -1,48 +1,55 @@
<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">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<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 v-else key="expand" class="sidebar-logo-link" to="/">
<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>
</transition>
</div>
</template>
<script>
import logoImg from '@/assets/logo/logo.png'
import variables from '@/assets/styles/variables.scss'
<script setup>
import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
import variables from '@/assets/styles/variables.module.scss'
export default {
name: 'SidebarLogo',
props: {
defineProps({
collapse: {
type: Boolean,
required: true
}
},
computed: {
variables() {
return variables
},
sideTheme() {
return this.$store.state.settings.sideTheme
},
navType() {
return this.$store.state.settings.navType
})
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)'
}
},
data() {
return {
title: process.env.VUE_APP_TITLE,
logo: logoImg
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>
<style lang="scss" scoped>
@@ -59,7 +66,7 @@ export default {
position: relative;
height: 50px;
line-height: 50px;
background: #2b2f3a;
background: v-bind(getLogoBackground);
text-align: center;
overflow: hidden;
@@ -77,7 +84,7 @@ export default {
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
color: v-bind(getLogoTextColor);
font-weight: 600;
line-height: 50px;
font-size: 14px;
@@ -1,17 +1,20 @@
<template>
<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)">
<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" />
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<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>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="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>
<sidebar-item
v-for="(child, index) in item.children"
:key="child.path + index"
@@ -20,22 +23,16 @@
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</el-sub-menu>
</div>
</template>
<script>
import path from 'path'
<script setup>
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
import { getNormalPath } from '@/utils/ruoyi'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
const props = defineProps({
// route object
item: {
type: Object,
@@ -49,13 +46,11 @@ export default {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
})
const onlyOneChild = ref({})
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = []
}
@@ -63,8 +58,7 @@ export default {
if (item.hidden) {
return false
}
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
onlyOneChild.value = item
return true
})
@@ -75,25 +69,32 @@ export default {
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath, routeQuery) {
}
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery)
return { path: path.resolve(this.basePath, routePath), query: query }
}
return path.resolve(this.basePath, routePath)
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>
@@ -1,16 +1,17 @@
<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" />
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:background-color="getMenuBackground"
:text-color="getMenuTextColor"
:unique-opened="true"
:active-text-color="settings.theme"
:active-text-color="theme"
:collapse-transition="false"
mode="vertical"
:class="sideTheme"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
@@ -23,35 +24,81 @@
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex"
import Logo from "./Logo"
import SidebarItem from "./SidebarItem"
import variables from "@/assets/styles/variables.scss"
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
export default {
components: { SidebarItem, Logo },
computed: {
...mapState(["settings"]),
...mapGetters(["sidebarRouters", "sidebar"]),
activeMenu() {
const route = this.$route
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const showLogo = computed(() => settingsStore.sidebarLogo)
const sideTheme = computed(() => settingsStore.sideTheme)
const theme = computed(() => settingsStore.theme)
const isCollapse = computed(() => !appStore.sidebar.opened)
// 获取菜单背景色
const getMenuBackground = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-bg)'
}
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
})
// 获取菜单文字颜色
const getMenuTextColor = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-text)'
}
return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
})
const activeMenu = computed(() => {
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
})
</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);
}
}
}
</script>
</style>
@@ -1,33 +1,39 @@
<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 />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4
<script setup>
import useTagsViewStore from '@/store/modules/tagsView'
export default {
name: 'ScrollPane',
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 tagAndTagSpacing = ref(4)
const { proxy } = getCurrentInstance()
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
const emits = defineEmits(['scroll', '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
@@ -40,7 +46,6 @@ export default {
return -c / 2 * (t * (t - 2) - 1) + b
}
const emit = this.$emit.bind(this)
function step(timestamp) {
if (!startTime) startTime = timestamp
const elapsed = timestamp - startTime
@@ -49,101 +54,103 @@ export default {
requestAnimationFrame(step)
} else {
$scrollWrapper.scrollLeft = target
emit('updateArrows')
emits('updateArrows')
}
}
requestAnimationFrame(step)
},
handleScroll(e) {
}
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
const $scrollWrapper = scrollWrapper.value
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
this.$emit('updateArrows')
},
emitScroll() {
this.$emit('scroll')
this.$emit('updateArrows')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
emits('updateArrows')
}
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.visitedViews)
function moveToTarget(currentTag) {
const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
const $scrollWrapper = scrollWrapper.value
let firstTag = null
let lastTag = null
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0]
lastTag = visitedViews.value[visitedViews.value.length - 1]
}
if (firstTag === currentTag) {
this.smoothScrollTo(0)
smoothScrollTo(0)
} else if (lastTag === currentTag) {
this.smoothScrollTo($scrollWrapper.scrollWidth - $containerWidth)
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
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
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
this.smoothScrollTo(afterNextTagOffsetLeft - $containerWidth)
smoothScrollTo(afterNextTagOffsetLeft - $containerWidth)
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
this.smoothScrollTo(beforePrevTagOffsetLeft)
smoothScrollTo(beforePrevTagOffsetLeft)
}
}
},
// 向左滚动固定距离
scrollLeft() {
const $scrollWrapper = this.scrollWrapper
this.smoothScrollTo(Math.max(0, $scrollWrapper.scrollLeft - 200))
},
// 向右滚动固定距离
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
}
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>
<style lang="scss" scoped>
<style lang='scss' scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep {
.el-scrollbar__bar {
:deep(.el-scrollbar__bar) {
bottom: 0px;
}
.el-scrollbar__wrap {
:deep(.el-scrollbar__wrap) {
height: 34px;
display: flex;
align-items: center;
}
}
}
</style>
+295 -343
View File
@@ -1,197 +1,181 @@
<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">
<i class="el-icon-arrow-left" />
<el-icon><arrow-left /></el-icon>
</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
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:data-path="tag.path"
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
:style="tagActiveStyle(tag)"
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag, $event)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@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;" />
{{ 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>
</scroll-pane>
<!-- 右切换箭头 -->
<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>
<!-- 下拉操作菜单 -->
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
<span class="tags-action-btn">
<i class="el-icon-arrow-down" />
<el-icon><arrow-down /></el-icon>
</span>
<el-dropdown-menu slot="dropdown" class="tags-dropdown-menu">
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close" icon="el-icon-close">关闭当前</el-dropdown-item>
<el-dropdown-item command="closeOthers" icon="el-icon-circle-close">关闭其他</el-dropdown-item>
<el-dropdown-item command="closeLeft" :disabled="isFirstView()" icon="el-icon-back">关闭左侧</el-dropdown-item>
<el-dropdown-item command="closeRight" :disabled="isLastView()" icon="el-icon-right">关闭</el-dropdown-item>
<el-dropdown-item command="closeAll" icon="el-icon-circle-close">全部关闭</el-dropdown-item>
<template #dropdown>
<el-dropdown-menu class="tags-dropdown-menu">
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close"><close style="width: 1em; height: 1em;" />关闭当前</el-dropdown-item>
<el-dropdown-item command="closeOthers"><circle-close style="width: 1em; height: 1em;" />关闭其他</el-dropdown-item>
<el-dropdown-item command="closeLeft" :disabled="isFirstView()"><back style="width: 1em; height: 1em;" />关闭</el-dropdown-item>
<el-dropdown-item command="closeRight" :disabled="isLastView()"><right style="width: 1em; height: 1em;" />关闭右侧</el-dropdown-item>
<el-dropdown-item command="closeAll"><circle-close style="width: 1em; height: 1em;" />全部关闭</el-dropdown-item>
<el-dropdown-item command="fullscreen" divided>
<template v-if="!isFullscreen"><i class="el-icon-full-screen"></i>全屏显示</template>
<template v-else><i class="el-icon-close"></i>退出全屏</template>
<template v-if="!isFullscreen"><full-screen style="width: 1em; height: 1em;" />全屏显示</template>
<template v-else><close style="width: 1em; height: 1em;" />退出全屏</template>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 刷新按钮 -->
<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>
<!-- 右键上下文菜单 -->
<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 v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li>
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li>
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li>
<li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em;" />刷新页面</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em;" />关闭当前</li>
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em;" />关闭其他</li>
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em;" />关闭左侧</li>
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em;" />关闭右侧</li>
<li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em;" />全部关闭</li>
</ul>
</div>
</template>
<script>
<script setup>
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 {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
canScrollLeft: false,
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) {
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const selectedTag = ref({})
const affixTags = ref([])
const scrollPaneRef = ref(null)
const canScrollLeft = ref(false)
const canScrollRight = ref(false)
const isFullscreen = ref(false)
const hiddenElements = ref([])
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
const theme = computed(() => useSettingsStore().theme)
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
const tagsViewPersist = computed(() => useSettingsStore().tagsViewPersist)
const tagsViewStyle = computed(() => useSettingsStore().tagsViewStyle)
// 下拉菜单针对当前激活的 tag
const selectedDropdownTag = computed(() => visitedViews.value.find(v => isActive(v)) || {})
watch(route, () => {
addTags()
moveToCurrentTag()
})
watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', this.closeMenu)
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
document.body.removeEventListener('click', 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) {
})
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' && this.isFullscreen) {
this.toggleFullscreen()
if (event.key === 'Escape' && isFullscreen.value) {
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 {}
}
function isActive(r) {
return r.path === route.path
}
function tagActiveStyle(tag) {
if (!isActive(tag) || tagsViewStyle.value !== 'card') return {}
return {
"background-color": this.theme,
"border-color": this.theme
'background-color': theme.value,
'border-color': theme.value
}
},
isAffix(tag) {
}
function isAffix(tag) {
return tag && tag.meta && tag.meta.affix
},
isFirstView() {
}
function isFirstView() {
try {
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
return tag.fullPath === '/index' || tag.fullPath === this.visitedViews[1].fullPath
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
}
},
isLastView() {
}
function isLastView() {
try {
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
return tag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
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
}
},
filterAffixTags(routes, basePath = '/') {
}
function filterAffixTags(routes, basePath = '') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
const tagPath = getNormalPath(basePath + '/' + route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
@@ -200,192 +184,189 @@ export default {
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
const tempTags = 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')
}
function initTags() {
if (tagsViewPersist.value) {
useTagsViewStore().loadPersistedViews()
}
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
const res = filterAffixTags(routes.value)
affixTags.value = res
for (const tag of res) {
if (tag.name) {
this.$store.dispatch('tagsView/addAffixView', tag)
useTagsViewStore().addAffixView(tag)
}
}
},
addTags() {
const { name } = this.$route
}
function addTags() {
const { name } = route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
useTagsViewStore().addView(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)
}
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)
}
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
}
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
}
})
},
toggleFullscreen() {
}
function toggleFullscreen() {
const mainContainer = document.querySelector('.main-container')
const navbar = document.querySelector('.navbar')
const sidebar = document.querySelector('.sidebar-container')
if (!mainContainer) return
if (!this.isFullscreen) {
if (!isFullscreen.value) {
mainContainer.classList.add('fullscreen-mode')
document.body.style.overflow = 'hidden'
const elementsToHide = [
{ el: navbar, originalDisplay: (navbar && navbar.style.display) || '' },
{ el: sidebar, originalDisplay: (sidebar && sidebar.style.display) || '' }
]
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'
this.hiddenElements.push(item)
hiddenElements.value.push(item)
}
})
this.isFullscreen = true
isFullscreen.value = true
} else {
mainContainer.classList.remove('fullscreen-mode')
document.body.style.overflow = ''
this.hiddenElements.forEach(item => {
hiddenElements.value.forEach(item => {
if (item.el) {
item.el.style.display = item.originalDisplay
}
})
this.hiddenElements = []
const btn = document.querySelector('.tags-action-btn')
if (btn) btn.blur()
this.isFullscreen = false
hiddenElements.value = []
document.querySelector('.tags-action-btn').blur()
isFullscreen.value = false
}
},
handleDropdownCommand(command) {
const tag = this.selectedDropdownTag
this.selectedTag = tag
}
function handleDropdownCommand(command) {
const tag = selectedDropdownTag.value
selectedTag.value = tag
switch (command) {
case 'refresh':
this.refreshSelectedTag(tag)
break
case 'fullscreen':
this.toggleFullscreen()
break
case 'close':
this.closeSelectedTag(tag)
break
case 'closeOthers':
this.closeOthersTags()
break
case 'closeLeft':
this.closeLeftTags()
break
case 'closeRight':
this.closeRightTags()
break
case 'closeAll':
this.closeAllTags(tag)
break
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
}
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view)
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/delIframeView', this.$route)
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view)
if (route.meta.link) {
useTagsViewStore().delIframeView(route)
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(() => {})
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag()
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => { })
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag()
})
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
return
}
this.toLastView(visitedViews, view)
toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
router.push(latestView.fullPath)
} else {
if (view && view.name === 'Dashboard') {
this.$router.replace({ path: '/redirect' + view.fullPath })
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()
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>
<style lang="scss" scoped>
@@ -394,8 +375,8 @@ $tags-bar-height: 34px;
.tags-view-container {
height: $tags-bar-height;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
background: var(--tags-bg, #fff);
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
display: flex;
align-items: center;
overflow: hidden;
@@ -405,7 +386,7 @@ $tags-bar-height: 34px;
$btn-hover-bg: #f0f2f5;
$btn-hover-color: #303133;
$btn-disabled-color: #c0c4cc;
$divider: 1px solid #d8dce5;
$divider: 1px solid var(--tags-item-border, #d8dce5);
.tags-nav-btn {
flex-shrink: 0;
@@ -440,27 +421,33 @@ $tags-bar-height: 34px;
height: 100%;
.tags-view-item {
display: inline-block;
display: inline-flex;
align-items: center;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
border: 1px solid var(--tags-item-border, #d8dce5);
color: var(--tags-item-text, #495060);
background: var(--tags-item-bg, #fff);
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
border-radius: 3px;
text-decoration: none;
vertical-align: middle;
padding-top: 2px !important;
&:first-of-type { margin-left: 6px; }
&:last-of-type { margin-right: 15px; }
}
}
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
@@ -469,7 +456,7 @@ $tags-bar-height: 34px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
margin-right: 5px;
}
}
@@ -508,7 +495,7 @@ $tags-bar-height: 34px;
.contextmenu {
margin: 0;
background: #fff;
background: var(--el-bg-color-overlay, #fff);
z-index: 3000;
position: fixed;
list-style-type: none;
@@ -516,22 +503,28 @@ $tags-bar-height: 34px;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
color: var(--tags-item-text, #333);
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
border: 1px solid var(--el-border-color-light, #e4e7ed);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
background: var(--tags-item-hover, #eee);
}
}
}
&.tags-view-container--chrome {
--chrome-strip-bg: #ffffff;
--chrome-strip-border: #e4e7ed;
--chrome-tab-text: #606266;
--chrome-strip-border: var(--el-border-color-lighter, #e4e7ed);
--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;
background: var(--chrome-strip-bg);
@@ -566,7 +559,7 @@ $tags-bar-height: 34px;
border: none !important;
border-radius: 0;
background: transparent !important;
color: var(--chrome-tab-text) !important;
color: var(--chrome-tab-text);
padding-top: 0 !important;
box-shadow: none !important;
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;
}
&:first-of-type { margin-left: 6px; }
&:last-of-type { margin-right: 10px; }
&:first-of-type {
margin-left: 6px;
}
&:last-of-type {
margin-right: 10px;
}
&: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;
}
&:hover:not(.active) {
background: #f5f7fa !important;
background: var(--el-fill-color-light, #f5f7fa) !important;
border-radius: 6px 6px 0 0;
color: #303133 !important;
color: var(--el-text-color-primary, #303133);
}
&.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);
}
}
.el-icon-close {
margin-left: 3px;
&:before {
vertical-align: -2px;
}
}
}
}
}
@@ -645,77 +637,37 @@ $tags-bar-height: 34px;
<style lang="scss">
.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 {
.el-icon-close {
.tags-close-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
vertical-align: 2px;
margin-left: 4px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -3px;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
cursor: pointer;
.el-icon-close {
width: 1em;
height: 1em;
vertical-align: 0;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
&:hover {
background-color: #b4bccc;
background-color: var(--tags-close-hover, #b4bccc);
.el-icon-close {
color: #fff;
}
}
}
}
}
/* 页签全屏模式样式 */
@@ -1,62 +1,60 @@
<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" />
<el-submenu index="more" class="el-submenu__hide-arrow" v-if="moreRoutes.length > 0">
<template slot="title">更多菜单</template>
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
<template #title>
<span>更多菜单</span>
</template>
<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>
</template>
<script>
<script setup>
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 {
components: { SidebarItem },
data() {
return {
// 顶部栏初始数
visibleNumber: 5
}
},
computed: {
theme() {
return this.$store.state.settings.theme
},
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
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const theme = computed(() => settingsStore.theme)
const device = computed(() => appStore.device)
const activeMenu = computed(() => {
const { meta, path } = 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 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
this.visibleNumber = parseInt(width / 85)
}
}
visibleNumber.value = Math.max(1, parseInt(width / 85))
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
@@ -65,34 +63,37 @@ export default {
padding: 0 10px !important;
}
.el-menu--horizontal .el-menu--popup .el-menu-item:hover {
background-color: #f5f7fa !important;
.topbar-menu.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
/* submenu item */
.topbar-menu.el-menu--horizontal > .el-submenu .el-submenu__title {
.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 {
color: v-bind(theme);
}
/* sub-menu item */
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 47px !important;
line-height: 50px !important;
color: #303133;
margin: 0 15px !important;
color: #303133 !important;
margin: 0 15px -3px!important;
}
/* topbar more arrow */
.topbar-menu .el-submenu .el-submenu__icon-arrow {
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
display: block !important;
}
/* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-submenu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
height: 55px;
}
.el-menu--horizontal .el-menu .el-menu-item, .el-menu--horizontal .el-menu .el-submenu__title{
color: #303133;
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
height: 60px;
}
</style>
+106 -79
View File
@@ -3,6 +3,7 @@
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
:ellipsis="false"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
@@ -14,8 +15,8 @@
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" :key="visibleNumber" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
@@ -27,34 +28,39 @@
{{ item.meta.title }}
</el-menu-item>
</template>
</el-submenu>
</el-sub-menu>
</el-menu>
</template>
<script>
<script setup>
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']
export default {
data() {
return {
// 顶部栏初始数
visibleNumber: 5,
// 当前激活菜单的 index
currentIndex: undefined
}
},
computed: {
theme() {
return this.$store.state.settings.theme
},
// 顶部显示菜单
topMenus() {
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
// 主题颜色
const theme = computed(() => settingsStore.theme)
// 所有的路由信息
const routers = computed(() => permissionStore.topbarRouters)
// 顶部显示菜单
const topMenus = computed(() => {
let topMenus = []
this.routers.map((menu) => {
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === '/' && menu.children) {
@@ -65,16 +71,13 @@ export default {
}
})
return topMenus
},
// 所有的路由信息
routers() {
return this.$store.state.permission.topbarRouters
},
// 设置子路由
childrenMenus() {
var childrenMenus = []
this.routers.map((router) => {
for (var item in router.children) {
})
// 设置子路由
const childrenMenus = computed(() => {
let childrenMenus = []
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path
@@ -89,84 +92,90 @@ export default {
}
})
return constantRoutes.concat(childrenMenus)
},
// 默认激活的菜单
activeMenu() {
const path = this.$route.path
})
// 默认激活的菜单
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 (!this.$route.meta.link) {
if (!route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
this.$store.dispatch('app/toggleSideBarHide', false)
appStore.toggleSideBarHide(false)
}
} else if(!this.$route.children) {
} else if(!route.children) {
activePath = path
this.$store.dispatch('app/toggleSideBarHide', true)
appStore.toggleSideBarHide(true)
}
this.activeRoutes(activePath)
activeRoutes(activePath)
return activePath
},
},
beforeMount() {
window.addEventListener('resize', this.setVisibleNumber)
},
beforeDestroy() {
window.removeEventListener('resize', this.setVisibleNumber)
},
mounted() {
this.setVisibleNumber()
},
methods: {
// 根据宽度计算设置显示栏数
setVisibleNumber() {
})
function 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)
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 = this.childrenMenus.find(item => item.path === key)
const routeMenu = childrenMenus.value.find(item => item.path === key)
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query)
this.$router.push({ path: key, query: query })
router.push({ path: key, query: query })
} else {
this.$router.push({ path: key })
router.push({ path: key })
}
this.$store.dispatch('app/toggleSideBarHide', true)
appStore.toggleSideBarHide(true)
} else {
// 显示左侧联动菜单
this.activeRoutes(key)
this.$store.dispatch('app/toggleSideBarHide', false)
activeRoutes(key)
appStore.toggleSideBarHide(false)
}
},
// 当前激活的路由
activeRoutes(key) {
var routes = []
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => {
}
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) {
this.$store.commit("SET_SIDEBAR_ROUTERS", routes)
permissionStore.setSidebarRouters(routes)
} else {
this.$store.dispatch('app/toggleSideBarHide', true)
}
}
appStore.toggleSideBarHide(true)
}
return routes
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal {
height: 50px !important;
border-bottom: none;
}
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
@@ -176,13 +185,13 @@ export default {
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;
color: #303133;
}
/* submenu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
@@ -190,4 +199,22 @@ export default {
padding: 0 5px !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>
-1
View File
@@ -1,5 +1,4 @@
export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue'
+70 -69
View File
@@ -1,72 +1,73 @@
<template>
<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"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar @setLayout="setLayout"/>
<tags-view v-if="needTagsView"/>
<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"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView" />
</div>
<app-main/>
<settings ref="settingRef"/>
<app-main />
<settings ref="settingRef" />
</div>
</div>
</template>
<script>
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
<script setup>
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
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()
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme)
const sidebar = computed(() => useAppStore().sidebar)
const device = computed(() => useAppStore().device)
const needTagsView = computed(() => settingsStore.tagsView)
const fixedHeader = computed(() => settingsStore.fixedHeader)
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile'
}))
const { width, height } = useWindowSize()
const WIDTH = 992 // refer to Bootstrap's responsive design
watch(() => device.value, () => {
if (device.value === 'mobile' && sidebar.value.opened) {
useAppStore().closeSideBar({ withoutAnimation: false })
}
})
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>
<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss";
@use "@/assets/styles/mixin.scss" as mix;
@use "@/assets/styles/variables.module.scss" as vars;
.app-wrapper {
@include clearfix;
.app-wrapper {
@include mix.clearfix;
position: relative;
height: 100%;
width: 100%;
@@ -75,14 +76,14 @@ export default {
position: fixed;
top: 0;
}
}
}
.main-container:has(.fixed-header) {
.main-container:has(.fixed-header) {
height: 100vh;
overflow: hidden;
}
}
.drawer-bg {
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
@@ -90,26 +91,26 @@ export default {
height: 100%;
position: absolute;
z-index: 999;
}
}
.fixed-header {
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
width: calc(100% - #{vars.$base-sidebar-width});
transition: width 0.28s;
}
}
.hideSidebar .fixed-header {
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
}
.sidebarHide .fixed-header {
.sidebarHide .fixed-header {
width: 100%;
}
}
.mobile .fixed-header {
.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
View File
@@ -1,28 +1,38 @@
import Vue from 'vue'
import { createApp } from 'vue'
import Cookies from 'js-cookie'
import Element from 'element-ui'
import './assets/styles/element-variables.scss'
import ElementPlus from 'element-plus'
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/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
// 注册指令
import plugins from './plugins' // plugins
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 { getDicts } from "@/api/system/dict/data"
import { useDict } from '@/utils/dict'
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"
// 文件上传组件
@@ -33,51 +43,42 @@ import ImageUpload from "@/components/ImageUpload"
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 字典数据组件
import DictData from '@/components/DictData'
const app = createApp(App)
// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download
app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.getConfigKey = getConfigKey
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
app.component('DictTag', DictTag)
app.component('Pagination', Pagination)
app.component('FileUpload', FileUpload)
app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor)
Vue.use(directive)
Vue.use(plugins)
DictData.install()
app.use(router)
app.use(store)
app.use(plugins)
app.use(elementIcons)
app.component('svg-icon', SvgIcon)
/**
* 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! ! !
*/
directive(app)
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
})
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
app.mount('#app')
+40 -34
View File
@@ -1,11 +1,14 @@
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isPathMatch } from '@/utils/validate'
import { isHttp, isPathMatch } from '@/utils/validate'
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 })
@@ -15,53 +18,56 @@ const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path))
}
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
const isLock = store.getters.isLock
/* has token*/
to.meta.title && useSettingsStore().setTitle(to.meta.title)
const isLock = useLockStore().isLock
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (isWhiteList(to.path)) {
next()
} else if (isLock && to.path !== '/lock') {
next({ path: '/lock' })
return { path: '/' }
}
if (isWhiteList(to.path)) {
return true
}
if (isLock && to.path !== '/lock') {
NProgress.done()
} else if (!isLock && to.path === '/lock') {
next({ path: '/' })
return { path: '/lock' }
}
if (!isLock && to.path === '/lock') {
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
return { path: '/' }
}
if (useUserStore().roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
try {
// 拉取user_info信息
await useUserStore().getInfo()
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
// 根据roles权限生成可访问的路由
const accessRoutes = await usePermissionStore().generateRoutes()
accessRoutes.forEach(route => {
if (!isHttp(route.path)) {
router.addRoute(route)
}
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
// 重新导航到目标路由,确保动态路由已注册
return { ...to, replace: true }
} catch (err) {
await useUserStore().logOut()
ElMessage.error(err)
return { path: '/' }
}
}
return true
} else {
// 没有token
if (isWhiteList(to.path)) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
NProgress.done()
return true
}
NProgress.done()
return `/login?redirect=${to.fullPath}` // 否则全部重定向到登录页
}
})
+3 -3
View File
@@ -1,8 +1,8 @@
import store from '@/store'
import useUserStore from '@/store/modules/user'
function authPermission(permission) {
const all_permission = "*:*:*"
const permissions = store.getters && store.getters.permissions
const permissions = useUserStore().permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
@@ -14,7 +14,7 @@ function authPermission(permission) {
function authRole(role) {
const super_admin = "admin"
const roles = store.getters && store.getters.roles
const roles = useUserStore().roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role

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