升级V3
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
# 告诉EditorConfig插件,这是根文件,不用继续往上查找
|
||||
root = true
|
||||
|
||||
# 匹配全部文件
|
||||
[*]
|
||||
# 设置字符集
|
||||
charset = utf-8
|
||||
# 缩进风格,可选space、tab
|
||||
indent_style = space
|
||||
# 缩进的空格数
|
||||
indent_size = 2
|
||||
# 结尾换行符,可选lf、cr、crlf
|
||||
end_of_line = lf
|
||||
# 在文件结尾插入新行
|
||||
insert_final_newline = true
|
||||
# 删除一行中的前后空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# 匹配md结尾的文件
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,11 +1,8 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
|
||||
# 开发环境配置
|
||||
ENV = 'development'
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
# 若依管理系统/开发环境
|
||||
VUE_APP_BASE_API = '/dev-api'
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
|
||||
# 生产环境配置
|
||||
ENV = 'production'
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
# 若依管理系统/生产环境
|
||||
VUE_APP_BASE_API = '/prod-api'
|
||||
VITE_APP_BASE_API = '/prod-api'
|
||||
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
||||
@@ -1,12 +1,11 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
|
||||
BABEL_ENV = production
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'staging'
|
||||
|
||||
NODE_ENV = production
|
||||
# 若依管理系统/生产环境
|
||||
VITE_APP_BASE_API = '/stage-api'
|
||||
|
||||
# 测试环境配置
|
||||
ENV = 'staging'
|
||||
|
||||
# 若依管理系统/测试环境
|
||||
VUE_APP_BASE_API = '/stage-api'
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
||||
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 RuoYi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+108
-16
@@ -1,30 +1,122 @@
|
||||
## 开发
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.2</h1>
|
||||
<h4 align="center">基于SpringBoot+Vue3前后端分离的Java快速开发框架</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.2-brightgreen.svg"></a>
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||
</p>
|
||||
|
||||
## 平台简介
|
||||
|
||||
* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
||||
* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast) 版本。
|
||||
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)
|
||||
|
||||
# 版本对比
|
||||
|
||||
RuoYi-Vue 前端项目的三个主要演进版本,方便你直观对比其技术栈差异(并行开发维护)。
|
||||
|
||||
| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
|
||||
| **脚本语言** | JavaScript | JavaScript | TypeScript |
|
||||
| **构建工具** | Vue CLI | Vite | Vite |
|
||||
| **UI 组件库** | Element UI | Element Plus | Element Plus |
|
||||
| **状态管理** | Vuex | Pinia | Pinia |
|
||||
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
|
||||
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
|
||||
| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) |
|
||||
|
||||
## 前端运行
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
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群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
'env': {
|
||||
'development': {
|
||||
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
|
||||
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
|
||||
'plugins': ['dynamic-import-node']
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@ echo.
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
npm run build:prod
|
||||
yarn build:prod
|
||||
|
||||
pause
|
||||
@@ -7,6 +7,6 @@ echo.
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
yarn --registry=https://registry.npmmirror.com
|
||||
|
||||
pause
|
||||
@@ -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
|
||||
@@ -1,35 +0,0 @@
|
||||
const { run } = require('runjs')
|
||||
const chalk = require('chalk')
|
||||
const config = require('../vue.config.js')
|
||||
const rawArgv = process.argv.slice(2)
|
||||
const args = rawArgv.join(' ')
|
||||
|
||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
||||
const report = rawArgv.includes('--report')
|
||||
|
||||
run(`vue-cli-service build ${args}`)
|
||||
|
||||
const port = 9526
|
||||
const publicPath = config.publicPath
|
||||
|
||||
var connect = require('connect')
|
||||
var serveStatic = require('serve-static')
|
||||
const app = connect()
|
||||
|
||||
app.use(
|
||||
publicPath,
|
||||
serveStatic('./dist', {
|
||||
index: ['index.html', '/']
|
||||
})
|
||||
)
|
||||
|
||||
app.listen(port, function () {
|
||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
||||
if (report) {
|
||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
run(`vue-cli-service build ${args}`)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<!--[if lt IE 11
|
||||
]><script>
|
||||
window.location.href = "/html/ie.html";
|
||||
</script><!
|
||||
[endif]-->
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.chromeframe {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: -75px 0 0 -75px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #fff;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
-ms-animation: spin 2s linear infinite;
|
||||
-moz-animation: spin 2s linear infinite;
|
||||
-o-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#loader:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #fff;
|
||||
-webkit-animation: spin 3s linear infinite;
|
||||
-moz-animation: spin 3s linear infinite;
|
||||
-o-animation: spin 3s linear infinite;
|
||||
-ms-animation: spin 3s linear infinite;
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
#loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #fff;
|
||||
-moz-animation: spin 1.5s linear infinite;
|
||||
-o-animation: spin 1.5s linear infinite;
|
||||
-ms-animation: spin 1.5s linear infinite;
|
||||
-webkit-animation: spin 1.5s linear infinite;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 51%;
|
||||
height: 100%;
|
||||
background: #7171c6;
|
||||
z-index: 1000;
|
||||
-webkit-transform: translateX(0);
|
||||
-ms-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-left {
|
||||
-webkit-transform: translateX(-100%);
|
||||
-ms-transform: translateX(-100%);
|
||||
transform: translateX(-100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-right {
|
||||
-webkit-transform: translateX(100%);
|
||||
-ms-transform: translateX(100%);
|
||||
transform: translateX(100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.loaded #loader {
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper {
|
||||
visibility: hidden;
|
||||
-webkit-transform: translateY(-100%);
|
||||
-ms-transform: translateY(-100%);
|
||||
transform: translateY(-100%);
|
||||
-webkit-transition: all 0.3s 1s ease-out;
|
||||
transition: all 0.3s 1s ease-out;
|
||||
}
|
||||
|
||||
.no-js #loader-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-js h1 {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: "Open Sans";
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
#svgContainer {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="loader-wrapper">
|
||||
<!-- <div id="loader"></div> -->
|
||||
<!-- <div class="loader-section section-left"></div> -->
|
||||
<!-- <div class="loader-section section-right"></div> -->
|
||||
<div id="svgContainer"></div>
|
||||
<!-- <div class="load_title">正在加载系统资源,请耐心等待</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="/src/plugins/lottie.min.js"></script>
|
||||
</body>
|
||||
<script>
|
||||
let svgContainer = document.getElementById("svgContainer");
|
||||
let animItem = bodymovin.loadAnimation({
|
||||
container: svgContainer,
|
||||
animType: "svg",
|
||||
loop: true,
|
||||
path: "/src/plugins/chatbot.json",
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
+33
-51
@@ -4,68 +4,50 @@
|
||||
"description": "若依管理系统",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
.chromeframe {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: -75px 0 0 -75px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
-ms-animation: spin 2s linear infinite;
|
||||
-moz-animation: spin 2s linear infinite;
|
||||
-o-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#loader:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 3s linear infinite;
|
||||
-moz-animation: spin 3s linear infinite;
|
||||
-o-animation: spin 3s linear infinite;
|
||||
-ms-animation: spin 3s linear infinite;
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
#loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-moz-animation: spin 1.5s linear infinite;
|
||||
-o-animation: spin 1.5s linear infinite;
|
||||
-ms-animation: spin 1.5s linear infinite;
|
||||
-webkit-animation: spin 1.5s linear infinite;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#loader-wrapper .loader-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 51%;
|
||||
height: 100%;
|
||||
background: #7171C6;
|
||||
z-index: 1000;
|
||||
-webkit-transform: translateX(0);
|
||||
-ms-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-left {
|
||||
-webkit-transform: translateX(-100%);
|
||||
-ms-transform: translateX(-100%);
|
||||
transform: translateX(-100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-right {
|
||||
-webkit-transform: translateX(100%);
|
||||
-ms-transform: translateX(100%);
|
||||
transform: translateX(100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader {
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper {
|
||||
visibility: hidden;
|
||||
-webkit-transform: translateY(-100%);
|
||||
-ms-transform: translateY(-100%);
|
||||
transform: translateY(-100%);
|
||||
-webkit-transition: all 0.3s 1s ease-out;
|
||||
transition: all 0.3s 1s ease-out;
|
||||
}
|
||||
|
||||
.no-js #loader-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-js h1 {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: 'Open Sans';
|
||||
color: #FFF;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="loader-wrapper">
|
||||
<div id="loader"></div>
|
||||
<div class="loader-section section-left"></div>
|
||||
<div class="loader-section section-right"></div>
|
||||
<div class="load_title">正在加载系统资源,请耐心等待</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
File diff suppressed because one or more lines are too long
+9
-14
@@ -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,5 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
import { parseStrEmpty } from "@/utils/ruoyi"
|
||||
import { parseStrEmpty } from "@/utils/ruoyi";
|
||||
|
||||
// 查询用户列表
|
||||
export function listUser(query) {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon'// svg component
|
||||
|
||||
// register globally
|
||||
Vue.component('svg-icon', SvgIcon)
|
||||
|
||||
const req = require.context('./svg', false, /\.svg$/)
|
||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||
requireAll(req)
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303018722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M368.832 67.2c51.328-16.384 89.216 34.112 75.712 76.416a346.816 346.816 0 0 0 435.84 435.84c42.304-13.44 92.8 24.384 76.48 75.712A467.968 467.968 0 1 1 368.832 67.2z m-35.776 122.688a368.832 368.832 0 1 0 501.056 501.056 445.952 445.952 0 0 1-501.056-501.056z" p-id="1448"></path></svg>
|
||||
|
After Width: | Height: | Size: 619 B |
@@ -1 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path fill="#5a5e66" d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
||||
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 226 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303115132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12397" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 890.432c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 0 1-66.816 0v-66.752c0-18.432 14.976-33.408 33.408-33.408z m-267.52-110.848a33.408 33.408 0 0 1 0 47.232l-47.296 47.232a33.408 33.408 0 0 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0z m582.336 0l47.232 47.232a33.408 33.408 0 0 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 1 1 47.232-47.232zM512 200.32a311.68 311.68 0 1 1 0 623.296 311.68 311.68 0 0 1 0-623.36z m0 66.752a244.864 244.864 0 1 0 0 489.728 244.864 244.864 0 0 0 0-489.728zM100.16 478.592a33.408 33.408 0 1 1 0 66.816H33.408a33.408 33.408 0 0 1 0-66.816h66.752z m890.432 0a33.408 33.408 0 0 1 0 66.816h-66.752a33.408 33.408 0 1 1 0-66.816h66.752zM197.184 149.952l47.232 47.232a33.408 33.408 0 1 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 0 1 47.232-47.232z m676.864 0a33.408 33.408 0 0 1 0 47.232l-47.232 47.232a33.408 33.408 0 1 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0zM512 0c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 1 1-66.816 0V33.408C478.592 14.976 493.568 0 512 0z" p-id="12398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,22 +0,0 @@
|
||||
# replace default config
|
||||
|
||||
# multipass: true
|
||||
# full: true
|
||||
|
||||
plugins:
|
||||
|
||||
# - name
|
||||
#
|
||||
# or:
|
||||
# - name: false
|
||||
# - name: true
|
||||
#
|
||||
# or:
|
||||
# - name:
|
||||
# param1: 1
|
||||
# param2: 2
|
||||
|
||||
- removeAttrs:
|
||||
attrs:
|
||||
- 'fill'
|
||||
- 'fill-rule'
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './variables.scss';
|
||||
@use './variables.module.scss' as *;
|
||||
|
||||
@mixin colorBtn($color) {
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -7,58 +7,45 @@
|
||||
.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;
|
||||
@@ -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: 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%;
|
||||
@@ -478,7 +488,7 @@
|
||||
background: #42b983!important;
|
||||
}
|
||||
|
||||
/* 表格右侧工具栏样式 */
|
||||
.top-right-btn {
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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: "",
|
||||
},
|
||||
})
|
||||
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))
|
||||
}
|
||||
},
|
||||
name: "vcrontab",
|
||||
props: ["expression", "hideComponent"],
|
||||
methods: {
|
||||
shouldHide(key) {
|
||||
if (this.hideComponent && this.hideComponent.includes(key)) return false
|
||||
return true
|
||||
},
|
||||
resolveExp() {
|
||||
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
|
||||
},
|
||||
function tabCheck(index) {
|
||||
tabActive.value = 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)
|
||||
function updateCrontabValue(name, value, from) {
|
||||
crontabValueObj.value[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) {
|
||||
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")
|
||||
},
|
||||
function hidePopup() {
|
||||
emit("hide")
|
||||
}
|
||||
// 填充表达式
|
||||
submitFill() {
|
||||
this.$emit("fill", this.crontabValueString)
|
||||
this.hidePopup()
|
||||
},
|
||||
clearCron() {
|
||||
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%;
|
||||
|
||||
@@ -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='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>
|
||||
@@ -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>
|
||||
@@ -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: {
|
||||
})
|
||||
const dayRule = ref('')
|
||||
const dayRuleSup = ref('')
|
||||
const dateArr = ref([])
|
||||
const resultList = ref([])
|
||||
const isShow = ref(false)
|
||||
watch(() => props.ex, () => expressionChange())
|
||||
// 表达式值变化时,开始去计算结果
|
||||
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
|
||||
|
||||
|
||||
},
|
||||
isShow.value = true
|
||||
}
|
||||
// 用于计算某位数字在数组中的索引
|
||||
getIndex(arr, value) {
|
||||
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) {
|
||||
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) {
|
||||
function compare(value1, value2) {
|
||||
if (value2 - value1 > 0) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
}
|
||||
// 格式化日期格式如:2017-9-19 18:04:33
|
||||
formatDate(value, type) {
|
||||
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) {
|
||||
function checkDate(value) {
|
||||
let time = new Date(value)
|
||||
let format = this.formatDate(time)
|
||||
let format = formatDate(time)
|
||||
return value === format
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'ex': 'expressionChange'
|
||||
},
|
||||
props: ['ex'],
|
||||
mounted: function () {
|
||||
// 初始化 获取一次结果
|
||||
this.expressionChange()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
expressionChange()
|
||||
})
|
||||
</script>
|
||||
@@ -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: {
|
||||
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
|
||||
}
|
||||
}
|
||||
// 单选按钮值变化时
|
||||
radioChange() {
|
||||
switch (this.radioValue) {
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -1,19 +1,19 @@
|
||||
<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"/>
|
||||
@@ -21,7 +21,7 @@
|
||||
</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"/> 年执行一次
|
||||
@@ -30,9 +30,9 @@
|
||||
</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,
|
||||
}
|
||||
@@ -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 ''
|
||||
return array.reduce((pre, cur) => {
|
||||
return pre + ' ' + cur
|
||||
})
|
||||
},
|
||||
|
||||
function handleArray(array) {
|
||||
if (array.length === 0) return ""
|
||||
return array.reduce((pre, cur) => {
|
||||
return pre + " " + cur
|
||||
})
|
||||
}
|
||||
|
||||
function isValueMatch(itemValue) {
|
||||
return values.value.some(val => val == itemValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-tag + .el-tag {
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -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)
|
||||
}, { immediate: true })
|
||||
|
||||
// 如果设置了上传地址则自定义图片上传事件
|
||||
if (this.type == 'url') {
|
||||
let toolbar = this.Quill.getModule("toolbar")
|
||||
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("图片插入失败")
|
||||
},
|
||||
}
|
||||
|
||||
// 上传失败处理
|
||||
function handleUploadError() {
|
||||
proxy.$modal.msgError("图片插入失败")
|
||||
}
|
||||
|
||||
// 复制粘贴图片处理
|
||||
handlePasteCapture(e) {
|
||||
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>仅允许导入xls、xlsx格式文件。</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()
|
||||
function handleClose() {
|
||||
isUploading.value = false
|
||||
selectedFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
},
|
||||
|
||||
// 下载模板
|
||||
handleDownloadTemplate() {
|
||||
this.download(this.templateAction, {}, `${this.templateFileName}_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
function handleDownloadTemplate() {
|
||||
proxy.download(props.templateAction, {}, `${props.templateFileName}_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
|
||||
// 上传进度
|
||||
handleProgress() {
|
||||
this.isUploading = true
|
||||
},
|
||||
function handleProgress() {
|
||||
isUploading.value = true
|
||||
}
|
||||
|
||||
/** 文件选择处理 */
|
||||
const handleFileChange = (file, fileList) => {
|
||||
selectedFile.value = file
|
||||
}
|
||||
|
||||
/** 文件删除处理 */
|
||||
const handleFileRemove = (file, fileList) => {
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
handleSuccess(response) {
|
||||
this.visible = false
|
||||
this.isUploading = false
|
||||
if (this.$refs.uploadRef) {
|
||||
this.$refs.uploadRef.clearFiles()
|
||||
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')
|
||||
}
|
||||
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('请选择要上传的文件。')
|
||||
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
|
||||
}
|
||||
const name = files[0].name.toLowerCase()
|
||||
if (!name.endsWith('.xls') && !name.endsWith('.xlsx')) {
|
||||
this.$modal.msgError('请选择后缀为 "xls" 或 "xlsx" 的文件。')
|
||||
return
|
||||
}
|
||||
this.$refs.uploadRef.submit()
|
||||
}
|
||||
}
|
||||
uploadRef.value.submit()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
@@ -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"> 删除</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: {
|
||||
},{ deep: true, immediate: true })
|
||||
|
||||
// 上传前校检格式和大小
|
||||
handleBeforeUpload(file) {
|
||||
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} 个!`)
|
||||
},
|
||||
function handleExceed() {
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
handleUploadError(err) {
|
||||
this.$modal.msgError("上传文件失败,请重试")
|
||||
this.$modal.closeLoading()
|
||||
},
|
||||
function handleUploadError(err) {
|
||||
proxy.$modal.msgError("上传文件失败")
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
handleUploadSuccess(res, file) {
|
||||
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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,103 +150,115 @@ 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
|
||||
options.value = searchPool.value
|
||||
}
|
||||
},
|
||||
activeStyle(index) {
|
||||
if (index !== this.activeIndex) return {}
|
||||
}
|
||||
|
||||
function activeStyle(index) {
|
||||
if (index !== activeIndex.value) return {}
|
||||
return {
|
||||
'background-color': this.theme,
|
||||
'color': '#fff'
|
||||
"background-color": theme.value,
|
||||
"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])
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
highlightText(text) {
|
||||
}
|
||||
|
||||
function selectActiveResult() {
|
||||
if (options.value.length > 0 && activeIndex.value >= 0) {
|
||||
change(options.value[activeIndex.value])
|
||||
}
|
||||
}
|
||||
|
||||
function highlightText(text) {
|
||||
if (!text) return ''
|
||||
if (!this.search) return text
|
||||
const keyword = this.escapeRegExp(this.search)
|
||||
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>')
|
||||
},
|
||||
escapeRegExp(str) {
|
||||
}
|
||||
|
||||
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 {
|
||||
.search-icon {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterIcons() {
|
||||
this.iconList = icons
|
||||
if (this.name) {
|
||||
this.iconList = this.iconList.filter(item => item.includes(this.name))
|
||||
}
|
||||
},
|
||||
selectedIcon(name) {
|
||||
this.$emit('selected', name)
|
||||
|
||||
function selectedIcon(name) {
|
||||
emit('selected', name)
|
||||
document.body.click()
|
||||
},
|
||||
reset() {
|
||||
this.name = ''
|
||||
this.iconList = icons
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
},{ deep: true, immediate: true })
|
||||
|
||||
// 上传前loading加载
|
||||
handleBeforeUpload(file) {
|
||||
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++
|
||||
},
|
||||
proxy.$modal.loading("正在上传图片,请稍候...")
|
||||
number.value++
|
||||
}
|
||||
|
||||
// 文件个数超出
|
||||
handleExceed() {
|
||||
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`)
|
||||
},
|
||||
function handleExceed() {
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
handleUploadSuccess(res, file) {
|
||||
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()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
function handleUploadError() {
|
||||
proxy.$modal.msgError("上传图片失败")
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
|
||||
// 预览
|
||||
handlePictureCardPreview(file) {
|
||||
this.dialogImageUrl = file.url
|
||||
this.dialogVisible = true
|
||||
},
|
||||
function handlePictureCardPreview(file) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
listToString(list, separator) {
|
||||
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
|
||||
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) : ''
|
||||
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>
|
||||
|
||||
|
||||
@@ -2,25 +2,22 @@
|
||||
<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,45 +57,40 @@ 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)
|
||||
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) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.$emit('pagination', { page: val, limit: this.pageSize })
|
||||
if (this.autoScroll) {
|
||||
emit('pagination', { page: currentPage.value, limit: val })
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(val) {
|
||||
emit('pagination', { page: val, limit: pageSize.value })
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
|
||||
<div class="pan-info">
|
||||
<div class="pan-info-roles-container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PanThumb',
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pan-item {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.pan-info-roles-container {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pan-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transform-origin: 95% 40%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* .pan-thumb:after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
top: 40%;
|
||||
left: 95%;
|
||||
margin: -4px 0 0 -4px;
|
||||
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
|
||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
|
||||
} */
|
||||
|
||||
.pan-info {
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pan-info h3 {
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
letter-spacing: 2px;
|
||||
font-size: 18px;
|
||||
margin: 0 60px;
|
||||
padding: 22px 0 0 0;
|
||||
height: 85px;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pan-info p {
|
||||
color: #fff;
|
||||
padding: 10px 5px;
|
||||
font-style: italic;
|
||||
margin: 0 30px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-info p a {
|
||||
display: block;
|
||||
color: #333;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
padding-top: 24px;
|
||||
margin: 7px auto 0;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
|
||||
transform: translateX(60px) rotate(90deg);
|
||||
}
|
||||
|
||||
.pan-info p a:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-thumb {
|
||||
transform: rotate(-110deg);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-info p a {
|
||||
opacity: 1;
|
||||
transform: translateX(0px) rotate(0deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg-icon icon-class="question" @click="goto" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RuoYiDoc',
|
||||
data() {
|
||||
return {
|
||||
url: 'http://doc.ruoyi.vip/ruoyi-vue'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goto() {
|
||||
window.open(this.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg-icon icon-class="github" @click="goto" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RuoYiGit',
|
||||
data() {
|
||||
return {
|
||||
url: 'https://gitee.com/y_project/RuoYi-Vue'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goto() {
|
||||
window.open(this.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,31 +2,33 @@
|
||||
<div class="top-right-btn" :style="style">
|
||||
<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]
|
||||
|
||||
// 是否全选/半选 状态
|
||||
const isChecked = computed({
|
||||
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
|
||||
set: () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
} 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: {
|
||||
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 })))
|
||||
|
||||
// 搜索
|
||||
toggleSearch() {
|
||||
let el = this.$el
|
||||
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")
|
||||
},
|
||||
function refresh() {
|
||||
emits("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 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()
|
||||
},
|
||||
saveStorage()
|
||||
}
|
||||
|
||||
// 打开显隐列dialog
|
||||
showColumn() {
|
||||
this.open = true
|
||||
},
|
||||
// 单勾选
|
||||
checkboxChange(event, key) {
|
||||
if (Array.isArray(this.columns)) {
|
||||
this.columns.filter(item => item.key == key)[0].visible = event
|
||||
} else {
|
||||
this.columns[key].visible = event
|
||||
function showColumn() {
|
||||
open.value = true
|
||||
}
|
||||
this.saveStorage()
|
||||
},
|
||||
// 切换全选/反选
|
||||
toggleCheckAll() {
|
||||
const newValue = !this.isChecked
|
||||
if (Array.isArray(this.columns)) {
|
||||
this.columns.forEach((col) => (col.visible = newValue))
|
||||
} else {
|
||||
Object.values(this.columns).forEach((col) => (col.visible = newValue))
|
||||
}
|
||||
this.saveStorage()
|
||||
},
|
||||
// 将当前列显隐状态持久化到 localStorage
|
||||
saveStorage() {
|
||||
if (!this.storageKey) return
|
||||
|
||||
// 如果传入了 storageKey,从 localStorage 恢复列显隐状态
|
||||
if (props.storageKey) {
|
||||
try {
|
||||
let state = {}
|
||||
if (Array.isArray(this.columns)) {
|
||||
this.columns.forEach((col, index) => { state[index] = col.visible })
|
||||
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 {
|
||||
Object.keys(this.columns).forEach(key => { state[key] = this.columns[key].visible })
|
||||
Object.keys(props.columns).forEach(key => {
|
||||
if (saved[key] !== undefined) props.columns[key].visible = saved[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
cache.local.setJSON(this.storageKey, state)
|
||||
} 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))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.keys(props.columns).forEach((key, index) => {
|
||||
if (props.columns[key].visible === false) {
|
||||
value.value.push(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 单勾选
|
||||
function checkboxChange(event, key) {
|
||||
if (Array.isArray(props.columns)) {
|
||||
props.columns.filter(item => item.key == key)[0].visible = event
|
||||
} else {
|
||||
props.columns[key].visible = event
|
||||
}
|
||||
saveStorage()
|
||||
}
|
||||
|
||||
// 切换全选/反选
|
||||
function toggleCheckAll() {
|
||||
const newValue = !isChecked.value
|
||||
if (Array.isArray(props.columns)) {
|
||||
props.columns.forEach((col) => (col.visible = newValue))
|
||||
} else {
|
||||
Object.values(props.columns).forEach((col) => (col.visible = newValue))
|
||||
}
|
||||
saveStorage()
|
||||
}
|
||||
|
||||
// 将当前列显隐状态持久化到 localStorage
|
||||
function saveStorage() {
|
||||
if (!props.storageKey) return
|
||||
try {
|
||||
let state = {}
|
||||
if (Array.isArray(props.columns)) {
|
||||
props.columns.forEach((col, index) => { state[index] = col.visible })
|
||||
} else {
|
||||
Object.keys(props.columns).forEach(key => { state[key] = props.columns[key].visible })
|
||||
}
|
||||
cache.local.setJSON(props.storageKey, state)
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
collapsed(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.handleCollapseChange(newVal);
|
||||
this.$emit("collapsed-change", newVal);
|
||||
}
|
||||
},
|
||||
// 监听内部展开状态变化,触发实际树的展开/收起
|
||||
expandedAll(newVal) {
|
||||
this.$nextTick(() => {
|
||||
if (newVal) {
|
||||
this.expandAllNodes();
|
||||
} else {
|
||||
this.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();
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.cleanup();
|
||||
},
|
||||
methods: {
|
||||
})
|
||||
|
||||
// 节点过滤方法
|
||||
filterNodeMethod(value, data) {
|
||||
if (this.filterMethod) {
|
||||
return this.filterMethod(value, data);
|
||||
const filterNodeMethod = (value, data) => {
|
||||
if (props.filterMethod) {
|
||||
return props.filterMethod(value, data)
|
||||
}
|
||||
if (!value) return true;
|
||||
return data.label && data.label.indexOf(value) !== -1;
|
||||
},
|
||||
// 清理定时器和动画帧
|
||||
cleanup() {
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
if (!value) return true
|
||||
return data.label && data.label.indexOf(value) !== -1
|
||||
}
|
||||
if (this.saveWidthTimer) {
|
||||
clearTimeout(this.saveWidthTimer);
|
||||
this.saveWidthTimer = null;
|
||||
|
||||
// 监听折叠状态
|
||||
watch(collapsed, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
handleCollapseChange(newVal)
|
||||
emit('collapsed-change', newVal)
|
||||
}
|
||||
},
|
||||
// 处理收起/展开状态变化
|
||||
handleCollapseChange(isCollapsed) {
|
||||
if (isCollapsed) {
|
||||
this.saveWidthToStorage();
|
||||
this.sidebarWidth = this.collapsedWidth;
|
||||
})
|
||||
|
||||
// 监听内部展开状态变化,触发实际树的展开/收起
|
||||
watch(expandedAll, (newVal) => {
|
||||
nextTick(() => {
|
||||
if (newVal) {
|
||||
expandAllNodes()
|
||||
} else {
|
||||
const savedWidth = this.getSavedWidth();
|
||||
this.sidebarWidth = savedWidth !== null ? savedWidth : this.defaultWidth;
|
||||
collapseAllNodes()
|
||||
}
|
||||
},
|
||||
})
|
||||
emit('expanded-all-change', newVal)
|
||||
})
|
||||
|
||||
// 监听搜索关键词
|
||||
watch(searchKeyword, (val) => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.filter(val)
|
||||
emit('search', val)
|
||||
}
|
||||
})
|
||||
|
||||
// 清理定时器和动画帧
|
||||
const cleanup = () => {
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = null
|
||||
}
|
||||
if (saveWidthTimer.value) {
|
||||
clearTimeout(saveWidthTimer.value)
|
||||
saveWidthTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理收起/展开状态变化
|
||||
const handleCollapseChange = (isCollapsed) => {
|
||||
if (isCollapsed) {
|
||||
saveWidthToStorage()
|
||||
sidebarWidth.value = props.collapsedWidth
|
||||
} else {
|
||||
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;
|
||||
},
|
||||
return null
|
||||
}
|
||||
|
||||
// 保存宽度到本地存储
|
||||
saveWidthToStorage() {
|
||||
if (this.collapsed || !this.enableStorage) return;
|
||||
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;
|
||||
},
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
// 切换展开/折叠所有节点
|
||||
toggleExpandAll() {
|
||||
this.isExpandedAll = !this.isExpandedAll;
|
||||
},
|
||||
const toggleExpandAll = () => {
|
||||
expandedAll.value = !expandedAll.value
|
||||
}
|
||||
|
||||
// 展开所有节点
|
||||
expandAllNodes() {
|
||||
if (!this.$refs.treeRef) return;
|
||||
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
|
||||
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;
|
||||
},
|
||||
}
|
||||
traverse(rootNode)
|
||||
return nodes
|
||||
}
|
||||
|
||||
// 收起所有节点
|
||||
collapseAllNodes() {
|
||||
if (!this.$refs.treeRef) return;
|
||||
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
|
||||
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");
|
||||
},
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 节点点击事件
|
||||
onNodeClick(data, node, e) {
|
||||
this.$emit("node-click", data, node, e);
|
||||
},
|
||||
const onNodeClick = (data, node, e) => {
|
||||
emit('node-click', data, node, e)
|
||||
}
|
||||
|
||||
// 复选框选中事件
|
||||
onCheck(data, checkedInfo) {
|
||||
this.$emit("check", data, checkedInfo);
|
||||
},
|
||||
const onCheck = (data, checkedInfo) => {
|
||||
emit('check', data, checkedInfo)
|
||||
}
|
||||
|
||||
// 节点展开事件
|
||||
onNodeExpand(data, node, e) {
|
||||
this.$emit("node-expand", data, node, e);
|
||||
},
|
||||
const onNodeExpand = (data, node, e) => {
|
||||
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 onNodeCollapse = (data, node, e) => {
|
||||
emit('node-collapse', data, node, e)
|
||||
}
|
||||
},
|
||||
// 获取当前选中的节点
|
||||
getCurrentNode() {
|
||||
if (this.$refs.treeRef) {
|
||||
return this.$refs.treeRef.getCurrentNode();
|
||||
|
||||
const setCurrentKey = (key) => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCurrentKey(key)
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 获取当前选中的节点的key
|
||||
getCurrentKey() {
|
||||
if (this.$refs.treeRef) {
|
||||
return this.$refs.treeRef.getCurrentKey();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 设置选中的节点keys(复选框)
|
||||
setCheckedKeys(keys) {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
this.$refs.treeRef.setCheckedKeys(keys);
|
||||
|
||||
const getCurrentNode = () => {
|
||||
if (treeRef.value) {
|
||||
return treeRef.value.getCurrentNode()
|
||||
}
|
||||
},
|
||||
// 获取选中的节点keys(复选框)
|
||||
getCheckedKeys() {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
return this.$refs.treeRef.getCheckedKeys();
|
||||
return null
|
||||
}
|
||||
return [];
|
||||
},
|
||||
// 获取选中的节点(复选框)
|
||||
getCheckedNodes() {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
return this.$refs.treeRef.getCheckedNodes();
|
||||
|
||||
const getCurrentKey = () => {
|
||||
if (treeRef.value) {
|
||||
return treeRef.value.getCurrentKey()
|
||||
}
|
||||
return [];
|
||||
},
|
||||
// 清空搜索
|
||||
clearSearch() {
|
||||
this.searchKeyword = "";
|
||||
if (this.$refs.treeRef) {
|
||||
this.$refs.treeRef.filter("");
|
||||
return null
|
||||
}
|
||||
},
|
||||
// 过滤树
|
||||
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 setCheckedKeys = (keys) => {
|
||||
if (treeRef.value && props.showCheckbox) {
|
||||
treeRef.value.setCheckedKeys(keys)
|
||||
}
|
||||
}
|
||||
|
||||
const getCheckedKeys = () => {
|
||||
if (treeRef.value && props.showCheckbox) {
|
||||
return treeRef.value.getCheckedKeys()
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getCheckedNodes = () => {
|
||||
if (treeRef.value && props.showCheckbox) {
|
||||
return treeRef.value.getCheckedNodes()
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
searchKeyword.value = ""
|
||||
if (treeRef.value) {
|
||||
treeRef.value.filter("")
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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;
|
||||
|
||||
const handleResizeMove = (e) => {
|
||||
if (!isResizing.value) return
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
}
|
||||
});
|
||||
},
|
||||
// 停止调整大小
|
||||
stopResize() {
|
||||
if (!this.isResizing) return;
|
||||
this.isResizing = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
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
|
||||
}
|
||||
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();
|
||||
})
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
if (!isResizing.value) return
|
||||
isResizing.value = false
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = null
|
||||
}
|
||||
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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* v-dialogDrag 弹窗拖拽
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
export default {
|
||||
bind(el, binding, vnode, oldVnode) {
|
||||
const value = binding.value
|
||||
if (value == false) return
|
||||
// 获取拖拽内容头部
|
||||
const dialogHeaderEl = el.querySelector('.el-dialog__header')
|
||||
const dragDom = el.querySelector('.el-dialog')
|
||||
dialogHeaderEl.style.cursor = 'move'
|
||||
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null)
|
||||
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
|
||||
dragDom.style.position = 'absolute'
|
||||
dragDom.style.marginTop = 0
|
||||
let width = dragDom.style.width
|
||||
if (width.includes('%')) {
|
||||
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100)
|
||||
} else {
|
||||
width = +width.replace(/\px/g, '')
|
||||
}
|
||||
dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`
|
||||
// 鼠标按下事件
|
||||
dialogHeaderEl.onmousedown = (e) => {
|
||||
// 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
|
||||
const disX = e.clientX - dialogHeaderEl.offsetLeft
|
||||
const disY = e.clientY - dialogHeaderEl.offsetTop
|
||||
|
||||
// 获取到的值带px 正则匹配替换
|
||||
let styL, styT
|
||||
|
||||
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
|
||||
if (sty.left.includes('%')) {
|
||||
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
|
||||
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
|
||||
} else {
|
||||
styL = +sty.left.replace(/\px/g, '')
|
||||
styT = +sty.top.replace(/\px/g, '')
|
||||
}
|
||||
|
||||
// 鼠标拖拽事件
|
||||
document.onmousemove = function (e) {
|
||||
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
|
||||
const l = e.clientX - disX
|
||||
const t = e.clientY - disY
|
||||
|
||||
let finallyL = l + styL
|
||||
let finallyT = t + styT
|
||||
|
||||
// 移动当前元素
|
||||
dragDom.style.left = `${finallyL}px`
|
||||
dragDom.style.top = `${finallyT}px`
|
||||
|
||||
}
|
||||
|
||||
document.onmouseup = function (e) {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* v-dialogDragWidth 可拖动弹窗高度(右下角)
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
export default {
|
||||
bind(el) {
|
||||
const dragDom = el.querySelector('.el-dialog')
|
||||
const lineEl = document.createElement('div')
|
||||
lineEl.style = 'width: 6px; background: inherit; height: 10px; position: absolute; right: 0; bottom: 0; margin: auto; z-index: 1; cursor: nwse-resize;'
|
||||
lineEl.addEventListener('mousedown',
|
||||
function(e) {
|
||||
// 鼠标按下,计算当前元素距离可视区的距离
|
||||
const disX = e.clientX - el.offsetLeft
|
||||
const disY = e.clientY - el.offsetTop
|
||||
// 当前宽度 高度
|
||||
const curWidth = dragDom.offsetWidth
|
||||
const curHeight = dragDom.offsetHeight
|
||||
document.onmousemove = function(e) {
|
||||
e.preventDefault() // 移动时禁用默认事件
|
||||
// 通过事件委托,计算移动的距离
|
||||
const xl = e.clientX - disX
|
||||
const yl = e.clientY - disY
|
||||
dragDom.style.width = `${curWidth + xl}px`
|
||||
dragDom.style.height = `${curHeight + yl}px`
|
||||
}
|
||||
document.onmouseup = function(e) {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}, false)
|
||||
dragDom.appendChild(lineEl)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* v-dialogDragWidth 可拖动弹窗宽度(右侧边)
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
export default {
|
||||
bind(el) {
|
||||
const dragDom = el.querySelector('.el-dialog')
|
||||
const lineEl = document.createElement('div')
|
||||
lineEl.style = 'width: 5px; background: inherit; height: 80%; position: absolute; right: 0; top: 0; bottom: 0; margin: auto; z-index: 1; cursor: w-resize;'
|
||||
lineEl.addEventListener('mousedown',
|
||||
function (e) {
|
||||
// 鼠标按下,计算当前元素距离可视区的距离
|
||||
const disX = e.clientX - el.offsetLeft
|
||||
// 当前宽度
|
||||
const curWidth = dragDom.offsetWidth
|
||||
document.onmousemove = function (e) {
|
||||
e.preventDefault() // 移动时禁用默认事件
|
||||
// 通过事件委托,计算移动的距离
|
||||
const l = e.clientX - disX
|
||||
dragDom.style.width = `${curWidth + l}px`
|
||||
}
|
||||
document.onmouseup = function (e) {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}, false)
|
||||
dragDom.appendChild(lineEl)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,9 @@
|
||||
import hasRole from './permission/hasRole'
|
||||
import 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(`请设置角色权限标签值`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 // 鼠标离开计时器
|
||||
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
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadNoticeTop()
|
||||
},
|
||||
methods: {
|
||||
|
||||
onMounted(() => loadNoticeTop())
|
||||
|
||||
// 鼠标移入铃铛区域
|
||||
onNoticeEnter() {
|
||||
clearTimeout(this.noticeLeaveTimer)
|
||||
this.noticeVisible = true
|
||||
this.$nextTick(() => {
|
||||
const popper = this.$refs.noticePopover.$refs.popper
|
||||
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
|
||||
})
|
||||
},
|
||||
function onNoticeLeave() {
|
||||
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 150)
|
||||
}
|
||||
|
||||
// 预览公告详情
|
||||
previewNotice(item) {
|
||||
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)
|
||||
},
|
||||
proxy.$refs["noticeViewRef"].open(item.noticeId)
|
||||
}
|
||||
|
||||
// 全部已读
|
||||
markAllRead() {
|
||||
const ids = this.noticeList.map(n => n.noticeId).join(',')
|
||||
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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
navType: {
|
||||
get() {
|
||||
return this.$store.state.settings.navType
|
||||
}
|
||||
},
|
||||
showLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
|
||||
function handleCommand(command) {
|
||||
switch (command) {
|
||||
case "setLayout":
|
||||
setLayout()
|
||||
break
|
||||
case "lockScreen":
|
||||
lockScreen()
|
||||
break
|
||||
case "logout":
|
||||
logout()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
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(() => { })
|
||||
}
|
||||
|
||||
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,7 +195,7 @@ export default {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
transition: background 0.3s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -165,7 +203,7 @@ export default {
|
||||
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;
|
||||
|
||||
@@ -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,282 +26,213 @@
|
||||
</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="">
|
||||
<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="">
|
||||
<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 />
|
||||
|
||||
<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-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
|
||||
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)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fixedHeader: {
|
||||
get() {
|
||||
return this.$store.state.settings.fixedHeader
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'fixedHeader',
|
||||
value: val
|
||||
})
|
||||
|
||||
function tagsViewPersistChange(val) {
|
||||
settingsStore.tagsViewPersist = val
|
||||
tagsViewPersist.value = val
|
||||
}
|
||||
},
|
||||
tagsViewPersist: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsViewPersist
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsViewPersist',
|
||||
value: val
|
||||
})
|
||||
|
||||
function themeChange(val) {
|
||||
settingsStore.theme = val
|
||||
handleThemeStyle(val)
|
||||
}
|
||||
},
|
||||
tagsView: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsView
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsView',
|
||||
value: val
|
||||
})
|
||||
|
||||
function handleTheme(val) {
|
||||
settingsStore.sideTheme = val
|
||||
sideTheme.value = val
|
||||
}
|
||||
},
|
||||
tagsIcon: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsIcon
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsIcon',
|
||||
value: val
|
||||
})
|
||||
|
||||
function handleNavType(val) {
|
||||
settingsStore.navType = val
|
||||
navType.value = val
|
||||
}
|
||||
},
|
||||
tagsViewStyle: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsViewStyle
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsViewStyle',
|
||||
value: val
|
||||
})
|
||||
|
||||
/** 菜单导航设置 */
|
||||
watch(() => navType, val => {
|
||||
if (val.value == 1) {
|
||||
appStore.sidebar.opened = true
|
||||
appStore.toggleSideBarHide(false)
|
||||
}
|
||||
},
|
||||
sidebarLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sidebarLogo',
|
||||
value: val
|
||||
})
|
||||
if (val.value == 2) {
|
||||
appStore.sidebar.opened = true
|
||||
}
|
||||
},
|
||||
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)
|
||||
if (val.value == 3) {
|
||||
appStore.sidebar.opened = false
|
||||
appStore.toggleSideBarHide(true)
|
||||
}
|
||||
},
|
||||
footerVisible: {
|
||||
get() {
|
||||
return this.$store.state.settings.footerVisible
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'footerVisible',
|
||||
value: val
|
||||
})
|
||||
if ([1, 3].includes(val.value)) {
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||
}
|
||||
}
|
||||
},
|
||||
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}"
|
||||
}`
|
||||
}, { immediate: true, deep: true }
|
||||
)
|
||||
setTimeout(this.$modal.closeLoading(), 1000)
|
||||
},
|
||||
resetSetting() {
|
||||
this.$modal.loading("正在清除设置缓存并刷新,请稍候...")
|
||||
this.$cache.local.remove('tags-view-visited')
|
||||
this.$cache.local.remove("layout-setting")
|
||||
|
||||
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 {
|
||||
<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;
|
||||
|
||||
.drawer-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox {
|
||||
@@ -339,29 +267,15 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
<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" />
|
||||
<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: getNormalPath(props.basePath + '/' + routePath), query: query }
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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
|
||||
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')
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
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>
|
||||
@@ -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) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
}
|
||||
},
|
||||
visitedViews() {
|
||||
this.$nextTick(() => {
|
||||
this.updateArrowState()
|
||||
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', closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', closeMenu)
|
||||
}
|
||||
},
|
||||
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,191 +184,188 @@ 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')
|
||||
}
|
||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||
for (const tag of affixTags) {
|
||||
|
||||
function initTags() {
|
||||
if (tagsViewPersist.value) {
|
||||
useTagsViewStore().loadPersistedViews()
|
||||
}
|
||||
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)
|
||||
}
|
||||
break
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
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)
|
||||
}
|
||||
},
|
||||
closeSelectedTag(view) {
|
||||
this.$tab.closePage(view).then(({ visitedViews }) => {
|
||||
if (this.isActive(view)) {
|
||||
this.toLastView(visitedViews, view)
|
||||
|
||||
function refreshSelectedTag(view) {
|
||||
proxy.$tab.refreshPage(view)
|
||||
if (route.meta.link) {
|
||||
useTagsViewStore().delIframeView(route)
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view) {
|
||||
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews, view)
|
||||
}
|
||||
})
|
||||
},
|
||||
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('/')
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
this.left = e.clientX
|
||||
this.top = e.clientY
|
||||
this.visible = true
|
||||
this.selectedTag = tag
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
this.updateArrowState()
|
||||
}
|
||||
|
||||
function 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>
|
||||
|
||||
@@ -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,78 +637,38 @@ $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页签全屏模式样式 */
|
||||
.main-container.fullscreen-mode {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
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)
|
||||
|
||||
// 顶部显示菜单
|
||||
topMenus() {
|
||||
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,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'
|
||||
|
||||
@@ -13,60 +13,61 @@
|
||||
</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'
|
||||
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 })
|
||||
}
|
||||
},
|
||||
variables() {
|
||||
return variables
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
},
|
||||
setLayout() {
|
||||
this.$refs.settingRef.openSetting()
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value - 1 < WIDTH) {
|
||||
useAppStore().toggleDevice('mobile')
|
||||
useAppStore().closeSideBar({ withoutAnimation: true })
|
||||
} else {
|
||||
useAppStore().toggleDevice('desktop')
|
||||
}
|
||||
})
|
||||
|
||||
function handleClickOutside() {
|
||||
useAppStore().closeSideBar({ withoutAnimation: false })
|
||||
}
|
||||
|
||||
const settingRef = ref(null)
|
||||
function setLayout() {
|
||||
settingRef.value.openSetting()
|
||||
}
|
||||
</script>
|
||||
|
||||
<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;
|
||||
@include mix.clearfix;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -97,7 +98,7 @@ export default {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$base-sidebar-width});
|
||||
width: calc(100% - #{vars.$base-sidebar-width});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import store from '@/store'
|
||||
|
||||
const { body } = document
|
||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
$route(route) {
|
||||
if (this.device === 'mobile' && this.sidebar.opened) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
mounted() {
|
||||
const isMobile = this.$_isMobile()
|
||||
if (isMobile) {
|
||||
store.dispatch('app/toggleDevice', 'mobile')
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// use $_ for mixins properties
|
||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
||||
$_isMobile() {
|
||||
const rect = body.getBoundingClientRect()
|
||||
return rect.width - 1 < WIDTH
|
||||
},
|
||||
$_resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = this.$_isMobile()
|
||||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
|
||||
|
||||
if (isMobile) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
-48
@@ -1,28 +1,38 @@
|
||||
import Vue from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import 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
@@ -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}` // 否则全部重定向到登录页
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user