切换为vue3的前端版本

pull/362/head
AlanScipio 2 years ago
parent 78e61d89ba
commit c3de97c825

@ -1,30 +1,14 @@
<p align="center"> # 平台简介
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-b99b286755aef70355a7084753f89cdb7c9.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.6.3</h1>
<h4 align="center">基于 Vue/Element UI 和 Spring Boot/Spring Cloud & Alibaba 前后端分离的分布式微服务架构</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Cloud/stargazers"><img src="https://gitee.com/y_project/RuoYi-Cloud/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Cloud"><img src="https://img.shields.io/badge/RuoYi-v3.6.3-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Cloud/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
## 平台简介 基于若依V3.6.3
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 采用前后端分离的模式,微服务版本前端(基于 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue))。 * 采用前后端分离的模式,微服务版本前端(基于 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue))。
* 后端采用Spring Boot、Spring Cloud & Alibaba。 * 后端采用Spring Boot、Spring Cloud & Alibaba。
* 注册中心、配置中心选型Nacos权限认证使用Redis。 * 注册中心、配置中心选型Nacos权限认证使用Redis。
* 流量控制框架选型Sentinel分布式事务选型Seata。 * 流量控制框架选型Sentinel分布式事务选型Seata。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Cloud-Vue3](https://github.com/yangzongzhuan/RuoYi-Cloud-Vue3),保持同步更新。 * 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Cloud-Vue3](https://github.com/yangzongzhuan/RuoYi-Cloud-Vue3),保持同步更新。
* 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
* 阿里云优惠券:[点我领取](https://www.aliyun.com/minisite/goods?userCode=brki8iof&share_source=copy_link),腾讯云优惠券:[点我领取](https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console)&nbsp;&nbsp;
#### 友情链接 [若依/RuoYi-Cloud](https://gitee.com/zhangmrit/ruoyi-cloud) Ant Design版本。
## 系统模块 # 系统模块
~~~ ~~~
com.ruoyi com.ruoyi
@ -52,11 +36,7 @@ com.ruoyi
├──pom.xml // 公共依赖 ├──pom.xml // 公共依赖
~~~ ~~~
## 架构图 # 内置功能
<img src="https://oscimg.oschina.net/oscnet/up-82e9722ecb846786405a904bafcf19f73f3.png"/>
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
@ -75,57 +55,3 @@ com.ruoyi
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。 15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 在线构建器拖动表单元素生成相应的HTML代码。 16. 在线构建器拖动表单元素生成相应的HTML代码。
17. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。 17. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
## 在线体验
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址http://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-4148b24f58660a9dc347761e4cf6162f28f.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/up-ece3fd37a3d4bb75a3926e905a3c5629055.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-92ffb7f3835855cff100fa0f754a6be0d99.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-ff9e3066561574aca73005c5730c6a41f15.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
</tr>
</table>
## 若依微服务交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-42799195-blue.svg)](https://jq.qq.com/?_wv=1027&k=yqInfq0S) [![加入QQ群](https://img.shields.io/badge/已满-170157040-blue.svg)](https://jq.qq.com/?_wv=1027&k=Oy1mb3p8) [![加入QQ群](https://img.shields.io/badge/已满-130643120-blue.svg)](https://jq.qq.com/?_wv=1027&k=rvxkJtXK) [![加入QQ群](https://img.shields.io/badge/已满-225920371-blue.svg)](https://jq.qq.com/?_wv=1027&k=0Ck3PvTe) [![加入QQ群](https://img.shields.io/badge/已满-201705537-blue.svg)](https://jq.qq.com/?_wv=1027&k=FnHHP4TT) [![加入QQ群](https://img.shields.io/badge/已满-236543183-blue.svg)](https://jq.qq.com/?_wv=1027&k=qdT1Ojpz) [![加入QQ群](https://img.shields.io/badge/已满-213618602-blue.svg)](https://jq.qq.com/?_wv=1027&k=nw3OiyXs) [![加入QQ群](https://img.shields.io/badge/已满-148794840-blue.svg)](https://jq.qq.com/?_wv=1027&k=kiU5WDls) [![加入QQ群](https://img.shields.io/badge/已满-118752664-blue.svg)](https://jq.qq.com/?_wv=1027&k=MtBy6YfT) [![加入QQ群](https://img.shields.io/badge/已满-101038945-blue.svg)](https://jq.qq.com/?_wv=1027&k=FqImHgH2) [![加入QQ群](https://img.shields.io/badge/已满-128355254-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G4jZ4EtdT50PhnMBudTnEwgonxkXOscJ&authKey=FkGHYfoTKlGE6wHdKdjH9bVoOgQjtLP9WM%2Fj7pqGY1msoqw9uxDiBo39E2mLgzYg&noverify=0&group_code=128355254) [![加入QQ群](https://img.shields.io/badge/179219821-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=irnwcXhbLOQEv1g-TwGifjNTA_f4wZiA&authKey=4bpzEwhcUY%2FvsPDHvzYn6xfoS%2FtOArvZ%2BGXzfr7O0%2FEqLfkKA%2BuCDXlzHIFg8t93&noverify=0&group_code=179219821) 点击按钮入群。

@ -9,14 +9,13 @@
<version>3.6.3</version> <version>3.6.3</version>
<name>ruoyi</name> <name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依微服务系统</description>
<properties> <properties>
<ruoyi.version>3.6.3</ruoyi.version> <ruoyi.version>3.6.3</ruoyi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<spring-boot.version>2.7.18</spring-boot.version> <spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version> <spring-cloud.version>2021.0.8</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version> <spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>

@ -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 = 若依管理系统DEV
# 开发环境配置 # 开发环境配置
ENV = 'development' VITE_APP_ENV = 'development'
# 若依管理系统/开发环境 # 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

@ -1,8 +1,11 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VITE_APP_TITLE = 若依管理系统PROD
# 生产环境配置 # 生产环境配置
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,10 +1,11 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VITE_APP_TITLE = 若依管理系统STAGING
NODE_ENV = production # 生产环境配置
VITE_APP_ENV = 'staging'
# 测试环境配置 # 若依管理系统/生产环境
ENV = 'staging' VITE_APP_BASE_API = '/stage-api'
# 若依管理系统/测试环境 # 是否在打包时开启压缩,支持 gzip 和 brotli
VUE_APP_BASE_API = '/stage-api' VITE_BUILD_COMPRESS = gzip

@ -1,10 +0,0 @@
# 忽略build目录下类型为js的文件的语法检查
build/*.js
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略public目录下文件的语法检查
public
# 忽略当前目录下为js的文件的语法检查
*.js
# 忽略当前目录下为vue的文件的语法检查
*.vue

@ -1,199 +0,0 @@
// ESlint 检查配置
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

@ -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.

@ -1,30 +1,143 @@
## 开发 <p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-b99b286755aef70355a7084753f89cdb7c9.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.6.3</h1>
<h4 align="center">基于 Vue3/Element Plus 和 Spring Boot/Spring Cloud & Alibaba 前后端分离的分布式微服务架构</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Cloud/stargazers"><img src="https://gitee.com/y_project/RuoYi-Cloud/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Cloud"><img src="https://img.shields.io/badge/RuoYi-v3.6.3-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Cloud/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-Cloud](https://gitee.com/y_project/RuoYi-Cloud) 或 [RuoYi-Cloud-Oracle](https://github.com/yangzongzhuan/RuoYi-Cloud-Oracle) 版本。
* 前端技术栈([Vue2](https://cn.vuejs.org) + [Element](https://github.com/ElemeFE/element) + [Vue CLI](https://cli.vuejs.org/zh)),请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud/tree/master/ruoyi-ui)。
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
* 阿里云优惠券:[点我领取](https://www.aliyun.com/minisite/goods?userCode=brki8iof&share_source=copy_link),腾讯云优惠券:[点我领取](https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console)&nbsp;&nbsp;
## 前端运行
```bash ```bash
# 克隆项目 # 克隆项目
git clone https://gitee.com/y_project/RuoYi-Vue git clone https://github.com/yangzongzhuan/RuoYi-Cloud-Vue3.git
# 进入项目目录 # 进入项目目录
cd ruoyi-ui cd RuoYi-Cloud-Vue3
# 安装依赖 # 安装依赖
npm install yarn --registry=https://registry.npmmirror.com
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com
# 启动服务 # 启动服务
npm run dev yarn dev
# 构建测试环境 yarn build:stage
# 构建生产环境 yarn build:prod
# 前端访问地址 http://localhost:80
``` ```
浏览器访问 http://localhost:80 ## 系统模块
## 发布 ~~~
com.ruoyi
├── ruoyi-ui // 前端框架 [80]
├── ruoyi-gateway // 网关模块 [8080]
├── ruoyi-auth // 认证中心 [9200]
├── ruoyi-api // 接口模块
│ └── ruoyi-api-system // 系统接口
├── ruoyi-common // 通用模块
│ └── ruoyi-common-core // 核心模块
│ └── ruoyi-common-datascope // 权限范围
│ └── ruoyi-common-datasource // 多数据源
│ └── ruoyi-common-log // 日志记录
│ └── ruoyi-common-redis // 缓存服务
│ └── ruoyi-common-security // 安全模块
│ └── ruoyi-common-swagger // 系统接口
├── ruoyi-modules // 业务模块
│ └── ruoyi-system // 系统模块 [9201]
│ └── ruoyi-gen // 代码生成 [9202]
│ └── ruoyi-job // 定时任务 [9203]
│ └── ruoyi-file // 文件服务 [9300]
├── ruoyi-visual // 图形化管理模块
│ └── ruoyi-visual-monitor // 监控中心 [9100]
├──pom.xml // 公共依赖
~~~
```bash ## 架构图
# 构建测试环境
npm run build:stage
# 构建生产环境 <img src="https://oscimg.oschina.net/oscnet/up-82e9722ecb846786405a904bafcf19f73f3.png"/>
npm run build:prod
``` ## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 代码生成前后端代码的生成java、html、xml、sql支持CRUD下载 。
14. 系统接口根据业务代码自动生成相关的api接口文档。
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 在线构建器拖动表单元素生成相应的HTML代码。
17. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
## 在线体验
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址http://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-4148b24f58660a9dc347761e4cf6162f28f.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/up-ece3fd37a3d4bb75a3926e905a3c5629055.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-92ffb7f3835855cff100fa0f754a6be0d99.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-ff9e3066561574aca73005c5730c6a41f15.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
</tr>
</table>
## 若依微服务交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-42799195-blue.svg)](https://jq.qq.com/?_wv=1027&k=yqInfq0S) [![加入QQ群](https://img.shields.io/badge/已满-170157040-blue.svg)](https://jq.qq.com/?_wv=1027&k=Oy1mb3p8) [![加入QQ群](https://img.shields.io/badge/已满-130643120-blue.svg)](https://jq.qq.com/?_wv=1027&k=rvxkJtXK) [![加入QQ群](https://img.shields.io/badge/已满-225920371-blue.svg)](https://jq.qq.com/?_wv=1027&k=0Ck3PvTe) [![加入QQ群](https://img.shields.io/badge/已满-201705537-blue.svg)](https://jq.qq.com/?_wv=1027&k=FnHHP4TT) [![加入QQ群](https://img.shields.io/badge/已满-236543183-blue.svg)](https://jq.qq.com/?_wv=1027&k=qdT1Ojpz) [![加入QQ群](https://img.shields.io/badge/已满-213618602-blue.svg)](https://jq.qq.com/?_wv=1027&k=nw3OiyXs) [![加入QQ群](https://img.shields.io/badge/已满-148794840-blue.svg)](https://jq.qq.com/?_wv=1027&k=kiU5WDls) [![加入QQ群](https://img.shields.io/badge/已满-118752664-blue.svg)](https://jq.qq.com/?_wv=1027&k=MtBy6YfT) [![加入QQ群](https://img.shields.io/badge/已满-101038945-blue.svg)](https://jq.qq.com/?_wv=1027&k=FqImHgH2) [![加入QQ群](https://img.shields.io/badge/已满-128355254-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G4jZ4EtdT50PhnMBudTnEwgonxkXOscJ&authKey=FkGHYfoTKlGE6wHdKdjH9bVoOgQjtLP9WM%2Fj7pqGY1msoqw9uxDiBo39E2mLgzYg&noverify=0&group_code=128355254) [![加入QQ群](https://img.shields.io/badge/179219821-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=irnwcXhbLOQEv1g-TwGifjNTA_f4wZiA&authKey=4bpzEwhcUY%2FvsPDHvzYn6xfoS%2FtOArvZ%2BGXzfr7O0%2FEqLfkKA%2BuCDXlzHIFg8t93&noverify=0&group_code=179219821) 点击按钮入群。

@ -1,13 +0,0 @@
module.exports = {
presets: [
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
'@vue/cli-plugin-babel/preset'
],
'env': {
'development': {
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
'plugins': ['dynamic-import-node']
}
}
}

@ -7,6 +7,6 @@ echo.
cd %~dp0 cd %~dp0
cd .. cd ..
npm run build:prod yarn build:prod
pause pause

@ -7,6 +7,6 @@ echo.
cd %~dp0 cd %~dp0
cd .. cd ..
npm install --registry=https://registry.npmmirror.com yarn --registry=https://registry.npmmirror.com
pause pause

@ -1,12 +1,12 @@
@echo off @echo off
echo. echo.
echo [信息] 使用 Vue CLI 命令运行 Web 工程。 echo [信息] 使用 Vite 命令运行 Web 工程。
echo. echo.
%~d0 %~d0
cd %~dp0 cd %~dp0
cd .. cd ..
npm run dev yarn dev
pause pause

@ -1,35 +0,0 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

@ -1,14 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="utf-8"> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta charset="utf-8">
<meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="renderer" content="webkit">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><%= webpackConfig.name %></title> <link rel="icon" href="/favicon.ico">
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <title>若依管理系统</title>
<style> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html, html,
body, body,
#app { #app {
@ -16,6 +17,7 @@
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
.chromeframe { .chromeframe {
margin: 0.2em 0; margin: 0.2em 0;
background: #ccc; background: #ccc;
@ -92,6 +94,7 @@
-ms-transform: rotate(0deg); -ms-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(360deg); -webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg); -ms-transform: rotate(360deg);
@ -105,6 +108,7 @@
-ms-transform: rotate(0deg); -ms-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(360deg); -webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg); -ms-transform: rotate(360deg);
@ -194,15 +198,18 @@
opacity: 0.5; opacity: 0.5;
} }
</style> </style>
</head> </head>
<body>
<div id="app"> <body>
<div id="loader-wrapper"> <div id="app">
<div id="loader"></div> <div id="loader-wrapper">
<div class="loader-section section-left"></div> <div id="loader"></div>
<div class="loader-section section-right"></div> <div class="loader-section section-left"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div> <div class="loader-section section-right"></div>
</div> <div class="load_title">正在加载系统资源,请耐心等待</div>
</div> </div>
</body> </div>
<script type="module" src="/src/main.js"></script>
</body>
</html> </html>

@ -4,87 +4,42 @@
"description": "若依管理系统", "description": "若依管理系统",
"author": "若依", "author": "若依",
"license": "MIT", "license": "MIT",
"type": "module",
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "vite",
"build:prod": "vue-cli-service build", "build:prod": "vite build",
"build:stage": "vue-cli-service build --mode staging", "build:stage": "vite build --mode staging",
"preview": "node build/index.js --preview", "preview": "vite preview"
"lint": "eslint --ext .js,.vue src"
}, },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
},
"keywords": [
"vue",
"admin",
"dashboard",
"element-ui",
"boilerplate",
"admin-template",
"management-system"
],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitee.com/y_project/RuoYi-Cloud.git" "url": "https://gitee.com/y_project/RuoYi-Cloud.git"
}, },
"dependencies": { "dependencies": {
"@riophae/vue-treeselect": "0.4.0", "@element-plus/icons-vue": "2.3.1",
"axios": "0.24.0", "@vueup/vue-quill": "1.2.0",
"clipboard": "2.0.8", "@vueuse/core": "10.6.1",
"core-js": "3.25.3", "axios": "0.27.2",
"echarts": "5.4.0", "echarts": "5.4.3",
"element-ui": "2.15.14", "element-plus": "2.4.3",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"fuse.js": "6.4.3", "fuse.js": "6.6.2",
"highlight.js": "9.18.5", "js-cookie": "3.0.5",
"js-beautify": "1.13.0", "jsencrypt": "3.3.2",
"js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "1.3.7", "pinia": "2.1.7",
"screenfull": "5.0.2", "vue": "3.3.9",
"sortablejs": "1.10.2", "vue-cropper": "1.1.1",
"vue": "2.6.12", "vue-router": "4.2.5"
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-meta": "2.4.0",
"vue-router": "3.4.9",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "4.4.6", "@vitejs/plugin-vue": "4.5.0",
"@vue/cli-plugin-eslint": "4.4.6", "@vue/compiler-sfc": "3.3.9",
"@vue/cli-service": "4.4.6", "sass": "1.69.5",
"babel-eslint": "10.1.0", "unplugin-auto-import": "0.17.1",
"babel-plugin-dynamic-import-node": "2.3.3", "vite": "5.0.4",
"chalk": "4.1.0", "vite-plugin-compression": "0.5.1",
"compression-webpack-plugin": "5.0.2", "vite-plugin-svg-icons": "2.0.1",
"connect": "3.6.6", "unplugin-vue-setup-extend-plus": "1.0.0"
"eslint": "7.15.0", }
"eslint-plugin-vue": "7.2.0",
"lint-staged": "10.5.3",
"runjs": "4.4.2",
"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"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
} }

@ -1,2 +0,0 @@
User-agent: *
Disallow: /

@ -1,28 +1,15 @@
<template> <template>
<div id="app"> <router-view />
<router-view />
<theme-picker />
</div>
</template> </template>
<script> <script setup>
import ThemePicker from "@/components/ThemePicker"; import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
export default { onMounted(() => {
name: "App", nextTick(() => {
components: { ThemePicker }, // ³õʼ»¯Ö÷ÌâÑùʽ
metaInfo() { handleThemeStyle(useSettingsStore().theme)
return { })
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title, })
titleTemplate: title => {
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
}
}
}
};
</script> </script>
<style scoped>
#app .theme-picker {
display: none;
}
</style>

@ -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="1605865043777" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="856" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="857"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 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'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

@ -1,4 +1,4 @@
@import './variables.scss'; @import './variables.module.scss';
@mixin colorBtn($color) { @mixin colorBtn($color) {
background: $color; background: $color;

@ -82,3 +82,15 @@
.el-range-separator { .el-range-separator {
box-sizing: content-box; box-sizing: content-box;
} }
.el-menu--collapse
> div
> .el-submenu
> .el-submenu__title
.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,14 @@
@import './variables.scss'; @import './variables.module.scss';
@import './mixin.scss'; @import './mixin.scss';
@import './transition.scss'; @import './transition.scss';
@import './element-ui.scss'; @import './element-ui.scss';
@import './sidebar.scss'; @import './sidebar.scss';
@import './btn.scss'; @import './btn.scss';
@import './ruoyi.scss';
body { body {
height: 100%; height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;

@ -1,143 +1,133 @@
/** /**
* css * css
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
/** 基础通用 **/ /** 基础通用 **/
.pt5 { .pt5 {
padding-top: 5px; padding-top: 5px;
} }
.pr5 { .pr5 {
padding-right: 5px; padding-right: 5px;
} }
.pb5 { .pb5 {
padding-bottom: 5px; padding-bottom: 5px;
} }
.mt5 { .mt5 {
margin-top: 5px; margin-top: 5px;
} }
.mr5 { .mr5 {
margin-right: 5px; margin-right: 5px;
} }
.mb5 { .mb5 {
margin-bottom: 5px; margin-bottom: 5px;
} }
.mb8 { .mb8 {
margin-bottom: 8px; margin-bottom: 8px;
} }
.ml5 { .ml5 {
margin-left: 5px; margin-left: 5px;
} }
.mt10 { .mt10 {
margin-top: 10px; margin-top: 10px;
} }
.mr10 { .mr10 {
margin-right: 10px; margin-right: 10px;
} }
.mb10 { .mb10 {
margin-bottom: 10px; margin-bottom: 10px;
} }
.ml10 { .ml10 {
margin-left: 10px; margin-left: 10px;
} }
.mt20 { .mt20 {
margin-top: 20px; margin-top: 20px;
} }
.mr20 { .mr20 {
margin-right: 20px; margin-right: 20px;
} }
.mb20 { .mb20 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.ml20 { .ml20 {
margin-left: 20px; margin-left: 20px;
} }
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
font-family: inherit; font-family: inherit;
font-weight: 500; font-weight: 500;
line-height: 1.1; line-height: 1.1;
color: inherit; color: inherit;
} }
.el-message-box__status + .el-message-box__message{ .el-form .el-form-item__label {
word-break: break-word; font-weight: 700;
} }
.el-dialog:not(.is-fullscreen) { .el-dialog:not(.is-fullscreen) {
margin-top: 6vh !important; margin-top: 6vh !important;
} }
.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { .el-dialog.scrollbar .el-dialog__body {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
max-height: 70vh; max-height: 70vh;
padding: 10px 20px 0; padding: 10px 20px 0;
} }
.el-table { .el-table {
.el-table__header-wrapper, .el-table__fixed-header-wrapper { .el-table__header-wrapper, .el-table__fixed-header-wrapper {
th { th {
word-break: break-word; word-break: break-word;
background-color: #f8f8f9; background-color: #f8f8f9 !important;
color: #515a6e; color: #515a6e;
height: 40px; height: 40px !important;
font-size: 13px; font-size: 13px;
} }
} }
.el-table__body-wrapper {
.el-table__body-wrapper { .el-button [class*="el-icon-"] + span {
.el-button [class*="el-icon-"] + span { margin-left: 1px;
margin-left: 1px; }
} }
}
} }
/** 表单布局 **/ /** 表单布局 **/
.form-header { .form-header {
font-size: 15px; font-size:15px;
color: #6379bb; color:#6379bb;
border-bottom: 1px solid #ddd; border-bottom:1px solid #ddd;
margin: 8px 10px 25px 10px; margin:8px 10px 25px 10px;
padding-bottom: 5px padding-bottom:5px
} }
/** 表格布局 **/ /** 表格布局 **/
.pagination-container { .pagination-container {
position: relative; position: relative;
height: 25px; height: 25px;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 15px; margin-top: 15px;
padding: 10px 20px !important; padding: 10px 20px !important;
}
.el-dialog .pagination-container {
position: static !important;
} }
/* tree border */ /* tree border */
.tree-border { .tree-border {
margin-top: 5px; margin-top: 5px;
border: 1px solid #e5e6e7; border: 1px solid #e5e6e7;
background: #FFFFFF none; background: #FFFFFF none;
border-radius: 4px; border-radius:4px;
width: 100%;
} }
.pagination-container .el-pagination { .pagination-container .el-pagination {
right: 0; right: 0;
position: absolute; position: absolute;
} }
@media (max-width: 768px) { @media ( max-width : 768px) {
.pagination-container .el-pagination > .el-pagination__jump { .pagination-container .el-pagination > .el-pagination__jump {
display: none !important; display: none !important;
} }
@ -146,64 +136,65 @@
} }
} }
.el-table .fixed-width .el-button--mini { .el-table .fixed-width .el-button--small {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
width: inherit; width: inherit;
} }
/** 表格更多操作下拉样式 */ /** 表格更多操作下拉样式 */
.el-table .el-dropdown-link,.el-table .el-dropdown-selfdefine { .el-table .el-dropdown-link {
cursor: pointer; cursor: pointer;
margin-left: 5px; color: #409EFF;
margin-left: 10px;
} }
.el-table .el-dropdown, .el-icon-arrow-down { .el-table .el-dropdown, .el-icon-arrow-down {
font-size: 12px; font-size: 12px;
} }
.el-tree-node__content > .el-checkbox { .el-tree-node__content > .el-checkbox {
margin-right: 8px; margin-right: 8px;
} }
.list-group-striped > .list-group-item { .list-group-striped > .list-group-item {
border-left: 0; border-left: 0;
border-right: 0; border-right: 0;
border-radius: 0; border-radius: 0;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
} }
.list-group { .list-group {
padding-left: 0px; padding-left: 0px;
list-style: none; list-style: none;
} }
.list-group-item { .list-group-item {
border-bottom: 1px solid #e7eaec; border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec; border-top: 1px solid #e7eaec;
margin-bottom: -1px; margin-bottom: -1px;
padding: 11px 0px; padding: 11px 0px;
font-size: 13px; font-size: 13px;
} }
.pull-right { .pull-right {
float: right !important; float: right !important;
} }
.el-card__header { .el-card__header {
padding: 14px 15px 7px; padding: 14px 15px 7px !important;
min-height: 40px; min-height: 40px;
} }
.el-card__body { .el-card__body {
padding: 15px 20px 20px 20px; padding: 15px 20px 20px 20px !important;
} }
.card-box { .card-box {
padding-right: 15px; padding-right: 15px;
padding-left: 15px; padding-left: 15px;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* button color */ /* button color */
@ -229,63 +220,62 @@
/* text color */ /* text color */
.text-navy { .text-navy {
color: #1ab394; color: #1ab394;
} }
.text-primary { .text-primary {
color: inherit; color: inherit;
} }
.text-success { .text-success {
color: #1c84c6; color: #1c84c6;
} }
.text-info { .text-info {
color: #23c6c8; color: #23c6c8;
} }
.text-warning { .text-warning {
color: #f8ac59; color: #f8ac59;
} }
.text-danger { .text-danger {
color: #ed5565; color: #ed5565;
} }
.text-muted { .text-muted {
color: #888888; color: #888888;
} }
/* image */ /* image */
.img-circle { .img-circle {
border-radius: 50%; border-radius: 50%;
} }
.img-lg { .img-lg {
width: 120px; width: 120px;
height: 120px; height: 120px;
} }
.avatar-upload-preview { .avatar-upload-preview {
position: relative; position: absolute;
top: 50%; top: 50%;
left: 50%; transform: translate(50%, -50%);
transform: translate(-50%, -50%); width: 200px;
width: 200px; height: 200px;
height: 200px; border-radius: 50%;
border-radius: 50%; box-shadow: 0 0 4px #ccc;
box-shadow: 0 0 4px #ccc; overflow: hidden;
overflow: hidden;
} }
/* 拖拽列样式 */ /* 拖拽列样式 */
.sortable-ghost { .sortable-ghost{
opacity: .8; opacity: .8;
color: #fff !important; color: #fff!important;
background: #42b983 !important; background: #42b983!important;
} }
/* 表格右侧工具栏样式 */
.top-right-btn { .top-right-btn {
position: relative; margin-left: auto;
float: right;
} }

@ -70,26 +70,30 @@
width: 100% !important; width: 100% !important;
} }
.el-menu-item, .el-submenu__title { .el-menu-item, .menu-title {
overflow: hidden !important; overflow: hidden !important;
text-overflow: ellipsis !important; text-overflow: ellipsis !important;
white-space: nowrap !important; white-space: nowrap !important;
} }
.el-menu-item .el-menu-tooltip__trigger {
display: inline-block !important;
}
// menu hover // menu hover
.submenu-title-noDropdown, .sub-menu-title-noDropdown,
.el-submenu__title { .el-sub-menu__title {
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.06) !important; background-color: rgba(0, 0, 0, 0.06) !important;
} }
} }
& .theme-dark .is-active > .el-submenu__title { & .theme-dark .is-active > .el-sub-menu__title {
color: $base-menu-color-active !important; color: $base-menu-color-active !important;
} }
& .nest-menu .el-submenu>.el-submenu__title, & .nest-menu .el-sub-menu>.el-sub-menu__title,
& .el-submenu .el-menu-item { & .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important; min-width: $base-sidebar-width !important;
&:hover { &:hover {
@ -97,8 +101,8 @@
} }
} }
& .theme-dark .nest-menu .el-submenu>.el-submenu__title, & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-submenu .el-menu-item { & .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important; background-color: $base-sub-menu-background !important;
&:hover { &:hover {
@ -116,7 +120,7 @@
margin-left: 54px; margin-left: 54px;
} }
.submenu-title-noDropdown { .sub-menu-title-noDropdown {
padding: 0 !important; padding: 0 !important;
position: relative; position: relative;
@ -129,10 +133,10 @@
} }
} }
.el-submenu { .el-sub-menu {
overflow: hidden; overflow: hidden;
&>.el-submenu__title { &>.el-sub-menu__title {
padding: 0 !important; padding: 0 !important;
.svg-icon { .svg-icon {
@ -143,8 +147,8 @@
} }
.el-menu--collapse { .el-menu--collapse {
.el-submenu { .el-sub-menu {
&>.el-submenu__title { &>.el-sub-menu__title {
&>span { &>span {
height: 0; height: 0;
width: 0; width: 0;
@ -152,12 +156,19 @@
visibility: hidden; visibility: hidden;
display: inline-block; display: inline-block;
} }
&>i {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
} }
} }
} }
} }
.el-menu--collapse .el-menu .el-submenu { .el-menu--collapse .el-menu .el-sub-menu {
min-width: $base-sidebar-width !important; min-width: $base-sidebar-width !important;
} }
@ -198,15 +209,15 @@
} }
} }
.nest-menu .el-submenu>.el-submenu__title, .nest-menu .el-sub-menu>.el-sub-menu__title,
.el-menu-item { .el-menu-item {
&:hover { &:hover {
// you can use $subMenuHover // you can use $sub-menuHover
background-color: rgba(0, 0, 0, 0.06) !important; background-color: rgba(0, 0, 0, 0.06) !important;
} }
} }
// the scroll bar appears when the subMenu is too long // the scroll bar appears when the sub-menu is too long
>.el-menu--popup { >.el-menu--popup {
max-height: 100vh; max-height: 100vh;
overflow-y: auto; overflow-y: auto;

@ -1,25 +1,25 @@
// base color // base color
$blue:#324157; $blue: #324157;
$light-blue:#3A71A8; $light-blue: #3A71A8;
$red:#C03639; $red: #C03639;
$pink: #E65D6E; $pink: #E65D6E;
$green: #30B08F; $green: #30B08F;
$tiffany: #4AB7BD; $tiffany: #4AB7BD;
$yellow:#FEC171; $yellow: #FEC171;
$panGreen: #30B08F; $panGreen: #30B08F;
// //
$base-menu-color:#bfcbd9; $base-menu-color: #bfcbd9;
$base-menu-color-active:#f4f4f5; $base-menu-color-active: #f4f4f5;
$base-menu-background:#304156; $base-menu-background: #304156;
$base-logo-title-color: #ffffff; $base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70); $base-menu-light-color: rgba(0, 0, 0, 0.7);
$base-menu-light-background:#ffffff; $base-menu-light-background: #ffffff;
$base-logo-light-title-color: #001529; $base-logo-light-title-color: #001529;
$base-sub-menu-background:#1f2d3d; $base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover:#001528; $base-sub-menu-hover: #001528;
// //
/** /**
@ -36,6 +36,12 @@ $base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528; $base-sub-menu-hover:#001528;
*/ */
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 200px; $base-sidebar-width: 200px;
// the :export directive is the magic sauce for webpack // the :export directive is the magic sauce for webpack
@ -50,5 +56,10 @@ $base-sidebar-width: 200px;
subMenuHover: $base-sub-menu-hover; subMenuHover: $base-sub-menu-hover;
sideBarWidth: $base-sidebar-width; sideBarWidth: $base-sidebar-width;
logoTitleColor: $base-logo-title-color; logoTitleColor: $base-logo-title-color;
logoLightTitleColor: $base-logo-light-title-color logoLightTitleColor: $base-logo-light-title-color;
primaryColor: $--color-primary;
successColor: $--color-success;
dangerColor: $--color-danger;
infoColor: $--color-info;
warningColor: $--color-warning;
} }

@ -9,57 +9,49 @@
</el-breadcrumb> </el-breadcrumb>
</template> </template>
<script> <script setup>
export default { const route = useRoute();
data() { const router = useRouter();
return { const levelList = ref([])
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() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) { function getBreadcrumb() {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched) // only show routes with meta.title
} let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0]
//
if (!isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}, }
isDashboard(route) { function isDashboard(route) {
const name = route && route.name const name = route && route.name
if (!name) { if (!name) {
return false return false
} }
return name.trim() === 'Index' return name.trim() === 'Index'
}, }
handleLink(item) { function handleLink(item) {
const { redirect, path } = item const { redirect, path } = item
if (redirect) { if (redirect) {
this.$router.push(redirect) router.push(redirect)
return 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> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.app-breadcrumb.el-breadcrumb { .app-breadcrumb.el-breadcrumb {
display: inline-block; display: inline-block;
font-size: 14px; font-size: 14px;

@ -1,161 +1,174 @@
<template> <template>
<el-form size="small"> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
允许的通配符[, - * ? / L W] 允许的通配符[, - * ? / L W]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
不指定 不指定
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
周期从 周期从
<el-input-number v-model='cycle01' :min="1" :max="30" /> - <el-input-number v-model='cycle01' :min="1" :max="30" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" /> <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" />
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="4"> <el-radio v-model='radioValue' :label="4">
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始 <el-input-number v-model='average01' :min="1" :max="30" /> 号开始
<el-input-number v-model='average02' :min="1" :max="31 - average01 || 1" /> 日执行一次 <el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="5"> <el-radio v-model='radioValue' :label="5">
每月 每月
<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日 <el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="6"> <el-radio v-model='radioValue' :label="6">
本月最后一天 本月最后一天
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="7"> <el-radio v-model='radioValue' :label="7">
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%"> <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 31" :key="item" :value="item">{{item}}</el-option> <el-option v-for="item in 31" :key="item" :label="item" :value="item" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script setup>
<script> const emit = defineEmits(['update'])
export default { const props = defineProps({
data() { cron: {
return { type: Object,
radioValue: 1, default: {
workday: 1, second: "*",
cycle01: 1, min: "*",
cycle02: 2, hour: "*",
average01: 1, day: "*",
average02: 1, month: "*",
checkboxList: [], week: "?",
checkNum: this.$options.propsData.check year: "",
} }
}, },
name: 'crontab-day', check: {
props: ['check', 'cron'], type: Function,
methods: { default: () => {
// }
radioChange() { }
('day rachange'); })
if (this.radioValue !== 2 && this.cron.week !== '?') { const radioValue = ref(1)
this.$emit('update', 'week', '?', 'day') const cycle01 = ref(1)
} const cycle02 = ref(2)
const average01 = ref(1)
switch (this.radioValue) { const average02 = ref(1)
case 1: const workday = ref(1)
this.$emit('update', 'day', '*'); const checkboxList = ref([])
break; const checkCopy = ref([1])
case 2: const cycleTotal = computed(() => {
this.$emit('update', 'day', '?'); cycle01.value = props.check(cycle01.value, 1, 30)
break; cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
case 3: return cycle01.value + '-' + cycle02.value
this.$emit('update', 'day', this.cycleTotal); })
break; const averageTotal = computed(() => {
case 4: average01.value = props.check(average01.value, 1, 30)
this.$emit('update', 'day', this.averageTotal); average02.value = props.check(average02.value, 1, 31 - average01.value)
break; return average01.value + '/' + average02.value
case 5: })
this.$emit('update', 'day', this.workday + 'W'); const workdayTotal = computed(() => {
break; workday.value = props.check(workday.value, 1, 31)
case 6: return workday.value + 'W'
this.$emit('update', 'day', 'L'); })
break; const checkboxString = computed(() => {
case 7: return checkboxList.value.join(',')
this.$emit('update', 'day', this.checkboxString); })
break; watch(() => props.cron.day, value => changeRadioValue(value))
} watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
('day rachange end'); function changeRadioValue(value) {
}, if (value === "*") {
// radioValue.value = 1
cycleChange() { } else if (value === "?") {
if (this.radioValue == '3') { radioValue.value = 2
this.$emit('update', 'day', this.cycleTotal); } else if (value.indexOf("-") > -1) {
} const indexArr = value.split('-')
}, cycle01.value = Number(indexArr[0])
// cycle02.value = Number(indexArr[1])
averageChange() { radioValue.value = 3
if (this.radioValue == '4') { } else if (value.indexOf("/") > -1) {
this.$emit('update', 'day', this.averageTotal); const indexArr = value.split('/')
} average01.value = Number(indexArr[0])
}, average02.value = Number(indexArr[1])
// radioValue.value = 4
workdayChange() { } else if (value.indexOf("W") > -1) {
if (this.radioValue == '5') { const indexArr = value.split("W")
this.$emit('update', 'day', this.workdayCheck + 'W'); workday.value = Number(indexArr[0])
} radioValue.value = 5
}, } else if (value === "L") {
// checkbox radioValue.value = 6
checkboxChange() { } else {
if (this.radioValue == '7') { checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
this.$emit('update', 'day', this.checkboxString); radioValue.value = 7
} }
} }
}, //
watch: { function onRadioChange() {
'radioValue': 'radioChange', if (radioValue.value === 2 && props.cron.week === '?') {
'cycleTotal': 'cycleChange', emit('update', 'week', '*', 'day')
'averageTotal': 'averageChange', }
'workdayCheck': 'workdayChange', if (radioValue.value !== 2 && props.cron.week !== '?') {
'checkboxString': 'checkboxChange', emit('update', 'week', '?', 'day')
}, }
computed: { switch (radioValue.value) {
// case 1:
cycleTotal: function () { emit('update', 'day', '*', 'day')
const cycle01 = this.checkNum(this.cycle01, 1, 30) break
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 31, 31) case 2:
return cycle01 + '-' + cycle02; emit('update', 'day', '?', 'day')
}, break
// case 3:
averageTotal: function () { emit('update', 'day', cycleTotal.value, 'day')
const average01 = this.checkNum(this.average01, 1, 30) break
const average02 = this.checkNum(this.average02, 1, 31 - average01 || 0) case 4:
return average01 + '/' + average02; emit('update', 'day', averageTotal.value, 'day')
}, break
// case 5:
workdayCheck: function () { emit('update', 'day', workdayTotal.value, 'day')
const workday = this.checkNum(this.workday, 1, 31) break
return workday; case 6:
}, emit('update', 'day', 'L', 'day')
// checkbox break
checkboxString: function () { case 7:
let str = this.checkboxList.join(); if (checkboxList.value.length === 0) {
return str == '' ? '*' : str; checkboxList.value.push(checkCopy.value[0])
} } else {
} checkCopy.value = checkboxList.value
}
emit('update', 'day', checkboxString.value, 'day')
break
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

@ -1,114 +1,127 @@
<template> <template>
<el-form size="small"> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
小时允许的通配符[, - * /] 小时允许的通配符[, - * /]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
周期从 周期从
<el-input-number v-model='cycle01' :min="0" :max="22" /> - <el-input-number v-model='cycle01' :min="0" :max="22" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" />
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="0" :max="22" /> 时开始 <el-input-number v-model='average01' :min="0" :max="22" /> 时开始
<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次 <el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="4"> <el-radio v-model='radioValue' :label="4">
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%"> <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 24" :key="item" :value="item-1">{{item-1}}</el-option> <el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(['update'])
data() { const props = defineProps({
return { cron: {
radioValue: 1, type: Object,
cycle01: 0, default: {
cycle02: 1, second: "*",
average01: 0, min: "*",
average02: 1, hour: "*",
checkboxList: [], day: "*",
checkNum: this.$options.propsData.check month: "*",
} week: "?",
}, year: "",
name: 'crontab-hour', }
props: ['check', 'cron'], },
methods: { check: {
// type: Function,
radioChange() { default: () => {
switch (this.radioValue) { }
case 1: }
this.$emit('update', 'hour', '*') })
break; const radioValue = ref(1)
case 2: const cycle01 = ref(0)
this.$emit('update', 'hour', this.cycleTotal); const cycle02 = ref(1)
break; const average01 = ref(0)
case 3: const average02 = ref(1)
this.$emit('update', 'hour', this.averageTotal); const checkboxList = ref([])
break; const checkCopy = ref([0])
case 4: const cycleTotal = computed(() => {
this.$emit('update', 'hour', this.checkboxString); cycle01.value = props.check(cycle01.value, 0, 22)
break; cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
} return cycle01.value + '-' + cycle02.value
}, })
// const averageTotal = computed(() => {
cycleChange() { average01.value = props.check(average01.value, 0, 22)
if (this.radioValue == '2') { average02.value = props.check(average02.value, 1, 23 - average01.value)
this.$emit('update', 'hour', this.cycleTotal); return average01.value + '/' + average02.value
} })
}, const checkboxString = computed(() => {
// return checkboxList.value.join(',')
averageChange() { })
if (this.radioValue == '3') { watch(() => props.cron.hour, value => changeRadioValue(value))
this.$emit('update', 'hour', this.averageTotal); watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
} function changeRadioValue(value) {
}, if (value === '*') {
// checkbox radioValue.value = 1
checkboxChange() { } else if (value.indexOf('-') > -1) {
if (this.radioValue == '4') { const indexArr = value.split('-')
this.$emit('update', 'hour', this.checkboxString); cycle01.value = Number(indexArr[0])
} cycle02.value = Number(indexArr[1])
} radioValue.value = 2
}, } else if (value.indexOf('/') > -1) {
watch: { const indexArr = value.split('/')
'radioValue': 'radioChange', average01.value = Number(indexArr[0])
'cycleTotal': 'cycleChange', average02.value = Number(indexArr[1])
'averageTotal': 'averageChange', radioValue.value = 3
'checkboxString': 'checkboxChange' } else {
}, checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
computed: { radioValue.value = 4
// }
cycleTotal: function () { }
const cycle01 = this.checkNum(this.cycle01, 0, 22) function onRadioChange() {
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23) switch (radioValue.value) {
return cycle01 + '-' + cycle02; case 1:
}, emit('update', 'hour', '*', 'hour')
// break
averageTotal: function () { case 2:
const average01 = this.checkNum(this.average01, 0, 22) emit('update', 'hour', cycleTotal.value, 'hour')
const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0) break
return average01 + '/' + average02; case 3:
}, emit('update', 'hour', averageTotal.value, 'hour')
// checkbox break
checkboxString: function () { case 4:
let str = this.checkboxList.join(); if (checkboxList.value.length === 0) {
return str == '' ? '*' : str; checkboxList.value.push(checkCopy.value[0])
} } else {
} checkCopy.value = checkboxList.value
}
emit('update', 'hour', checkboxString.value, 'hour')
break
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

@ -1,320 +1,233 @@
<template> <template>
<div> <div>
<el-tabs type="border-card"> <el-tabs type="border-card">
<el-tab-pane label="秒" v-if="shouldHide('second')"> <el-tab-pane label="秒" v-if="shouldHide('second')">
<CrontabSecond <CrontabSecond
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronsecond" ref="cronsecond"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')"> <el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin <CrontabMin
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronmin" ref="cronmin"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')"> <el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour <CrontabHour
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronhour" ref="cronhour"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="日" v-if="shouldHide('day')"> <el-tab-pane label="日" v-if="shouldHide('day')">
<CrontabDay <CrontabDay
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronday" ref="cronday"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="月" v-if="shouldHide('month')"> <el-tab-pane label="月" v-if="shouldHide('month')">
<CrontabMonth <CrontabMonth
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronmonth" ref="cronmonth"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="周" v-if="shouldHide('week')"> <el-tab-pane label="周" v-if="shouldHide('week')">
<CrontabWeek <CrontabWeek
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronweek" ref="cronweek"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="年" v-if="shouldHide('year')"> <el-tab-pane label="年" v-if="shouldHide('year')">
<CrontabYear <CrontabYear
@update="updateCrontabValue" @update="updateCrontabValue"
:check="checkNumber" :check="checkNumber"
:cron="crontabValueObj" :cron="crontabValueObj"
ref="cronyear" ref="cronyear"
/> />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="popup-main"> <div class="popup-main">
<div class="popup-result"> <div class="popup-result">
<p class="title">时间表达式</p> <p class="title">时间表达式</p>
<table> <table>
<thead> <thead>
<th v-for="item of tabTitles" width="40" :key="item">{{item}}</th> <th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th> <th>Cron 表达式</th>
</thead> </thead>
<tbody> <tbody>
<td> <td>
<span>{{crontabValueObj.second}}</span> <span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
<td> </td>
<span>{{crontabValueObj.min}}</span> <td>
</td> <span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<td> <el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
<span>{{crontabValueObj.hour}}</span> </td>
</td> <td>
<td> <span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<span>{{crontabValueObj.day}}</span> <el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td> </td>
<td> <td>
<span>{{crontabValueObj.month}}</span> <span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
<td> </td>
<span>{{crontabValueObj.week}}</span> <td>
</td> <span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<td> <el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
<span>{{crontabValueObj.year}}</span> </td>
</td> <td>
<td> <span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<span>{{crontabValueString}}</span> <el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td> </td>
</tbody> <td>
</table> <span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
</div> <el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
<CrontabResult :ex="crontabValueString"></CrontabResult> </td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn"> <div class="pop_btn">
<el-button size="small" type="primary" @click="submitFill"></el-button> <el-button type="primary" @click="submitFill"></el-button>
<el-button size="small" type="warning" @click="clearCron"></el-button> <el-button type="warning" @click="clearCron"></el-button>
<el-button size="small" @click="hidePopup"></el-button> <el-button @click="hidePopup"></el-button>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script setup>
import CrontabSecond from "./second.vue"; import CrontabSecond from "./second.vue"
import CrontabMin from "./min.vue"; import CrontabMin from "./min.vue"
import CrontabHour from "./hour.vue"; import CrontabHour from "./hour.vue"
import CrontabDay from "./day.vue"; import CrontabDay from "./day.vue"
import CrontabMonth from "./month.vue"; import CrontabMonth from "./month.vue"
import CrontabWeek from "./week.vue"; import CrontabWeek from "./week.vue"
import CrontabYear from "./year.vue"; import CrontabYear from "./year.vue"
import CrontabResult from "./result.vue"; import CrontabResult from "./result.vue"
const { proxy } = getCurrentInstance()
export default { const emit = defineEmits(['hide', 'fill'])
data() { const props = defineProps({
return { hideComponent: {
tabTitles: ["秒", "分钟", "小时", "日", "月", "周", "年"], type: Array,
tabActive: 0, default: () => [],
myindex: 0,
crontabValueObj: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
},
};
},
name: "vcrontab",
props: ["expression", "hideComponent"],
methods: {
shouldHide(key) {
if (this.hideComponent && this.hideComponent.includes(key)) return false;
return true;
}, },
resolveExp() { expression: {
// type: String,
if (this.expression) { default: ""
let arr = this.expression.split(" "); }
})
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
const tabActive = ref(0)
const hideComponent = ref([])
const expression = ref('')
const crontabValueObj = ref({
second: "*",
min: "*",
hour: "*",
day: "*",
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))
}
function resolveExp() {
//
if (expression.value) {
const arr = expression.value.split(/\s+/)
if (arr.length >= 6) { if (arr.length >= 6) {
//6 //6
let obj = { let obj = {
second: arr[0], second: arr[0],
min: arr[1], min: arr[1],
hour: arr[2], hour: arr[2],
day: arr[3], day: arr[3],
month: arr[4], month: arr[4],
week: arr[5], week: arr[5],
year: arr[6] ? arr[6] : "", year: arr[6] ? arr[6] : ""
}; }
this.crontabValueObj = { crontabValueObj.value = {
...obj, ...obj,
}; }
for (let i in obj) {
if (obj[i]) this.changeRadio(i, obj[i]);
}
} }
} else { } else {
// //
this.clearCron(); clearCron()
} }
}, }
// tab // tab
tabCheck(index) { function tabCheck(index) {
this.tabActive = index; tabActive.value = index
}, }
// //
updateCrontabValue(name, value, from) { function updateCrontabValue(name, value, from) {
"updateCrontabValue", name, value, from; crontabValueObj.value[name] = value
this.crontabValueObj[name] = value; }
if (from && from !== name) { // -props
console.log(`来自组件 ${from} 改变了 ${name} ${value}`); function checkNumber(value, minLimit, maxLimit) {
this.changeRadio(name, value); //
} value = Math.floor(value)
}, if (value < minLimit) {
// value = minLimit
changeRadio(name, value) { } else if (value > maxLimit) {
let arr = ["second", "min", "hour", "month"], value = maxLimit
refName = "cron" + name, }
insValue; return value
}
if (!this.$refs[refName]) return; //
function hidePopup() {
if (arr.includes(name)) { emit("hide")
if (value === "*") { }
insValue = 1; //
} else if (value.indexOf("-") > -1) { function submitFill() {
let indexArr = value.split("-"); emit("fill", crontabValueString.value)
isNaN(indexArr[0]) hidePopup()
? (this.$refs[refName].cycle01 = 0) }
: (this.$refs[refName].cycle01 = indexArr[0]); function clearCron() {
this.$refs[refName].cycle02 = indexArr[1]; //
insValue = 2; crontabValueObj.value = {
} else if (value.indexOf("/") > -1) {
let indexArr = value.split("/");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 3;
} else {
insValue = 4;
this.$refs[refName].checkboxList = value.split(",");
}
} else if (name == "day") {
if (value === "*") {
insValue = 1;
} else if (value == "?") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
let indexArr = value.split("-");
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0]);
this.$refs[refName].cycle02 = indexArr[1];
insValue = 3;
} else if (value.indexOf("/") > -1) {
let indexArr = value.split("/");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 4;
} else if (value.indexOf("W") > -1) {
let indexArr = value.split("W");
isNaN(indexArr[0])
? (this.$refs[refName].workday = 0)
: (this.$refs[refName].workday = indexArr[0]);
insValue = 5;
} else if (value === "L") {
insValue = 6;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 7;
}
} else if (name == "week") {
if (value === "*") {
insValue = 1;
} else if (value == "?") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
let indexArr = value.split("-");
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0]);
this.$refs[refName].cycle02 = indexArr[1];
insValue = 3;
} else if (value.indexOf("#") > -1) {
let indexArr = value.split("#");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 1)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 4;
} else if (value.indexOf("L") > -1) {
let indexArr = value.split("L");
isNaN(indexArr[0])
? (this.$refs[refName].weekday = 1)
: (this.$refs[refName].weekday = indexArr[0]);
insValue = 5;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 6;
}
} else if (name == "year") {
if (value == "") {
insValue = 1;
} else if (value == "*") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
insValue = 3;
} else if (value.indexOf("/") > -1) {
insValue = 4;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 5;
}
}
this.$refs[refName].radioValue = insValue;
},
// -props
checkNumber(value, minLimit, maxLimit) {
//
value = Math.floor(value);
if (value < minLimit) {
value = minLimit;
} else if (value > maxLimit) {
value = maxLimit;
}
return value;
},
//
hidePopup() {
this.$emit("hide");
},
//
submitFill() {
this.$emit("fill", this.crontabValueString);
this.hidePopup();
},
clearCron() {
//
("准备还原");
this.crontabValueObj = {
second: "*", second: "*",
min: "*", min: "*",
hour: "*", hour: "*",
@ -322,109 +235,76 @@ export default {
month: "*", month: "*",
week: "?", week: "?",
year: "", year: "",
}; }
for (let j in this.crontabValueObj) { }
this.changeRadio(j, this.crontabValueObj[j]); onMounted(() => {
} expression.value = props.expression
}, hideComponent.value = props.hideComponent
}, })
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();
},
};
</script> </script>
<style scoped>
<style lang="scss" scoped>
.pop_btn { .pop_btn {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
} }
.popup-main { .popup-main {
position: relative; position: relative;
margin: 10px auto; margin: 10px auto;
background: #fff; background: #fff;
border-radius: 5px; border-radius: 5px;
font-size: 12px; font-size: 12px;
overflow: hidden; overflow: hidden;
} }
.popup-title { .popup-title {
overflow: hidden; overflow: hidden;
line-height: 34px; line-height: 34px;
padding-top: 6px; padding-top: 6px;
background: #f2f2f2; background: #f2f2f2;
} }
.popup-result { .popup-result {
box-sizing: border-box; box-sizing: border-box;
line-height: 24px; line-height: 24px;
margin: 25px auto; margin: 25px auto;
padding: 15px 10px 10px; padding: 15px 10px 10px;
border: 1px solid #ccc; border: 1px solid #ccc;
position: relative; position: relative;
} }
.popup-result .title { .popup-result .title {
position: absolute; position: absolute;
top: -28px; top: -28px;
left: 50%; left: 50%;
width: 140px; width: 140px;
font-size: 14px; font-size: 14px;
margin-left: -70px; margin-left: -70px;
text-align: center; text-align: center;
line-height: 30px; line-height: 30px;
background: #fff; background: #fff;
} }
.popup-result table { .popup-result table {
text-align: center; text-align: center;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
}
.popup-result table td:not(.result) {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
} }
.popup-result table span { .popup-result table span {
display: block; display: block;
width: 100%; width: 100%;
font-family: arial; font-family: arial;
line-height: 30px; line-height: 30px;
height: 30px; height: 30px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
} }
.popup-result-scroll { .popup-result-scroll {
font-size: 12px; font-size: 12px;
line-height: 24px; line-height: 24px;
height: 10em; height: 10em;
overflow-y: auto; overflow-y: auto;
} }
</style> </style>

@ -1,116 +1,126 @@
<template> <template>
<el-form size="small"> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
分钟允许的通配符[, - * /] 分钟允许的通配符[, - * /]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
周期从 周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> - <el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟 <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始 <el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 分钟执行一次 <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
<el-form-item>
<el-radio v-model='radioValue' :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template> </template>
<script setup>
<script> const emit = defineEmits(['update'])
export default { const props = defineProps({
data() { cron: {
return { type: Object,
radioValue: 1, default: {
cycle01: 1, second: "*",
cycle02: 2, min: "*",
average01: 0, hour: "*",
average02: 1, day: "*",
checkboxList: [], month: "*",
checkNum: this.$options.propsData.check week: "?",
} year: "",
}, }
name: 'crontab-min', },
props: ['check', 'cron'], check: {
methods: { type: Function,
// default: () => {
radioChange() { }
switch (this.radioValue) { }
case 1: })
this.$emit('update', 'min', '*', 'min'); const radioValue = ref(1)
break; const cycle01 = ref(0)
case 2: const cycle02 = ref(1)
this.$emit('update', 'min', this.cycleTotal, 'min'); const average01 = ref(0)
break; const average02 = ref(1)
case 3: const checkboxList = ref([])
this.$emit('update', 'min', this.averageTotal, 'min'); const checkCopy = ref([0])
break; const cycleTotal = computed(() => {
case 4: cycle01.value = props.check(cycle01.value, 0, 58)
this.$emit('update', 'min', this.checkboxString, 'min'); cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
break; return cycle01.value + '-' + cycle02.value
} })
}, const averageTotal = computed(() => {
// average01.value = props.check(average01.value, 0, 58)
cycleChange() { average02.value = props.check(average02.value, 1, 59 - average01.value)
if (this.radioValue == '2') { return average01.value + '/' + average02.value
this.$emit('update', 'min', this.cycleTotal, 'min'); })
} const checkboxString = computed(() => {
}, return checkboxList.value.join(',')
// })
averageChange() { watch(() => props.cron.min, value => changeRadioValue(value))
if (this.radioValue == '3') { watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
this.$emit('update', 'min', this.averageTotal, 'min'); function changeRadioValue(value) {
} if (value === '*') {
}, radioValue.value = 1
// checkbox } else if (value.indexOf('-') > -1) {
checkboxChange() { const indexArr = value.split('-')
if (this.radioValue == '4') { cycle01.value = Number(indexArr[0])
this.$emit('update', 'min', this.checkboxString, 'min'); cycle02.value = Number(indexArr[1])
} radioValue.value = 2
}, } else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
}, average01.value = Number(indexArr[0])
watch: { average02.value = Number(indexArr[1])
'radioValue': 'radioChange', radioValue.value = 3
'cycleTotal': 'cycleChange', } else {
'averageTotal': 'averageChange', checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
'checkboxString': 'checkboxChange', radioValue.value = 4
}, }
computed: { }
// function onRadioChange() {
cycleTotal: function () { switch (radioValue.value) {
const cycle01 = this.checkNum(this.cycle01, 0, 58) case 1:
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59) emit('update', 'min', '*', 'min')
return cycle01 + '-' + cycle02; break
}, case 2:
// emit('update', 'min', cycleTotal.value, 'min')
averageTotal: function () { break
const average01 = this.checkNum(this.average01, 0, 58) case 3:
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0) emit('update', 'min', averageTotal.value, 'min')
return average01 + '/' + average02; break
}, case 4:
// checkbox if (checkboxList.value.length === 0) {
checkboxString: function () { checkboxList.value.push(checkCopy.value[0])
let str = this.checkboxList.join(); } else {
return str == '' ? '*' : str; checkCopy.value = checkboxList.value
} }
} emit('update', 'min', checkboxString.value, 'min')
break
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 19.8rem;
}
</style>

@ -1,114 +1,141 @@
<template> <template>
<el-form size='small'> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
允许的通配符[, - * /] 允许的通配符[, - * /]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
周期从 周期从
<el-input-number v-model='cycle01' :min="1" :max="11" /> - <el-input-number v-model='cycle01' :min="1" :max="11" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="12" /> <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" />
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="1" :max="11" /> 月开始 <el-input-number v-model='average01' :min="1" :max="11" /> 月开始
<el-input-number v-model='average02' :min="1" :max="12 - average01 || 0" /> 月月执行一次 <el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="4"> <el-radio v-model='radioValue' :label="4">
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%"> <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in 12" :key="item" :value="item">{{item}}</el-option> <el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(['update'])
data() { const props = defineProps({
return { cron: {
radioValue: 1, type: Object,
cycle01: 1, default: {
cycle02: 2, second: "*",
average01: 1, min: "*",
average02: 1, hour: "*",
checkboxList: [], day: "*",
checkNum: this.check month: "*",
} week: "?",
}, year: "",
name: 'crontab-month', }
props: ['check', 'cron'], },
methods: { check: {
// type: Function,
radioChange() { default: () => {
switch (this.radioValue) { }
case 1: }
this.$emit('update', 'month', '*'); })
break; const radioValue = ref(1)
case 2: const cycle01 = ref(1)
this.$emit('update', 'month', this.cycleTotal); const cycle02 = ref(2)
break; const average01 = ref(1)
case 3: const average02 = ref(1)
this.$emit('update', 'month', this.averageTotal); const checkboxList = ref([])
break; const checkCopy = ref([1])
case 4: const monthList = ref([
this.$emit('update', 'month', this.checkboxString); {key: 1, value: '一月'},
break; {key: 2, value: '二月'},
} {key: 3, value: '三月'},
}, {key: 4, value: '四月'},
// {key: 5, value: '五月'},
cycleChange() { {key: 6, value: '六月'},
if (this.radioValue == '2') { {key: 7, value: '七月'},
this.$emit('update', 'month', this.cycleTotal); {key: 8, value: '八月'},
} {key: 9, value: '九月'},
}, {key: 10, value: '十月'},
// {key: 11, value: '十一月'},
averageChange() { {key: 12, value: '十二月'}
if (this.radioValue == '3') { ])
this.$emit('update', 'month', this.averageTotal); const cycleTotal = computed(() => {
} cycle01.value = props.check(cycle01.value, 1, 11)
}, cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
// checkbox return cycle01.value + '-' + cycle02.value
checkboxChange() { })
if (this.radioValue == '4') { const averageTotal = computed(() => {
this.$emit('update', 'month', this.checkboxString); average01.value = props.check(average01.value, 1, 11)
} average02.value = props.check(average02.value, 1, 12 - average01.value)
} return average01.value + '/' + average02.value
}, })
watch: { const checkboxString = computed(() => {
'radioValue': 'radioChange', return checkboxList.value.join(',')
'cycleTotal': 'cycleChange', })
'averageTotal': 'averageChange', watch(() => props.cron.month, value => changeRadioValue(value))
'checkboxString': 'checkboxChange' watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
}, function changeRadioValue(value) {
computed: { if (value === '*') {
// radioValue.value = 1
cycleTotal: function () { } else if (value.indexOf('-') > -1) {
const cycle01 = this.checkNum(this.cycle01, 1, 11) const indexArr = value.split('-')
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 12) cycle01.value = Number(indexArr[0])
return cycle01 + '-' + cycle02; cycle02.value = Number(indexArr[1])
}, radioValue.value = 2
// } else if (value.indexOf('/') > -1) {
averageTotal: function () { const indexArr = value.split('/')
const average01 = this.checkNum(this.average01, 1, 11) average01.value = Number(indexArr[0])
const average02 = this.checkNum(this.average02, 1, 12 - average01 || 0) average02.value = Number(indexArr[1])
return average01 + '/' + average02; radioValue.value = 3
}, } else {
// checkbox checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
checkboxString: function () { radioValue.value = 4
let str = this.checkboxList.join(); }
return str == '' ? '*' : str; }
} function onRadioChange() {
} switch (radioValue.value) {
case 1:
emit('update', 'month', '*', 'month')
break
case 2:
emit('update', 'month', cycleTotal.value, 'month')
break
case 3:
emit('update', 'month', averageTotal.value, 'month')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'month', checkboxString.value, 'month')
break
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

File diff suppressed because it is too large Load Diff

@ -1,117 +1,128 @@
<template> <template>
<el-form size="small"> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
允许的通配符[, - * /] 允许的通配符[, - * /]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
周期从 周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> - <el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" />
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始 <el-input-number v-model='average01' :min="0" :max="58" /> 秒开始
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 秒执行一次 <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="4"> <el-radio v-model='radioValue' :label="4">
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%"> <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option> <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(['update'])
data() { const props = defineProps({
return { cron: {
radioValue: 1, type: Object,
cycle01: 1, default: {
cycle02: 2, second: "*",
average01: 0, min: "*",
average02: 1, hour: "*",
checkboxList: [], day: "*",
checkNum: this.$options.propsData.check month: "*",
} week: "?",
}, year: "",
name: 'crontab-second', }
props: ['check', 'radioParent'], },
methods: { check: {
// type: Function,
radioChange() { default: () => {
switch (this.radioValue) { }
case 1: }
this.$emit('update', 'second', '*', 'second'); })
break; const radioValue = ref(1)
case 2: const cycle01 = ref(0)
this.$emit('update', 'second', this.cycleTotal); const cycle02 = ref(1)
break; const average01 = ref(0)
case 3: const average02 = ref(1)
this.$emit('update', 'second', this.averageTotal); const checkboxList = ref([])
break; const checkCopy = ref([0])
case 4: const cycleTotal = computed(() => {
this.$emit('update', 'second', this.checkboxString); cycle01.value = props.check(cycle01.value, 0, 58)
break; cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
} return cycle01.value + '-' + cycle02.value
}, })
// const averageTotal = computed(() => {
cycleChange() { average01.value = props.check(average01.value, 0, 58)
if (this.radioValue == '2') { average02.value = props.check(average02.value, 1, 59 - average01.value)
this.$emit('update', 'second', this.cycleTotal); return average01.value + '/' + average02.value
} })
}, const checkboxString = computed(() => {
// return checkboxList.value.join(',')
averageChange() { })
if (this.radioValue == '3') { watch(() => props.cron.second, value => changeRadioValue(value))
this.$emit('update', 'second', this.averageTotal); watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
} function changeRadioValue(value) {
}, if (value === '*') {
// checkbox radioValue.value = 1
checkboxChange() { } else if (value.indexOf('-') > -1) {
if (this.radioValue == '4') { const indexArr = value.split('-')
this.$emit('update', 'second', this.checkboxString); cycle01.value = Number(indexArr[0])
} cycle02.value = Number(indexArr[1])
} radioValue.value = 2
}, } else if (value.indexOf('/') > -1) {
watch: { const indexArr = value.split('/')
'radioValue': 'radioChange', average01.value = Number(indexArr[0])
'cycleTotal': 'cycleChange', average02.value = Number(indexArr[1])
'averageTotal': 'averageChange', radioValue.value = 3
'checkboxString': 'checkboxChange', } else {
radioParent() { checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
this.radioValue = this.radioParent radioValue.value = 4
} }
}, }
computed: { //
// function onRadioChange() {
cycleTotal: function () { switch (radioValue.value) {
const cycle01 = this.checkNum(this.cycle01, 0, 58) case 1:
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59) emit('update', 'second', '*', 'second')
return cycle01 + '-' + cycle02; break
}, case 2:
// emit('update', 'second', cycleTotal.value, 'second')
averageTotal: function () { break
const average01 = this.checkNum(this.average01, 0, 58) case 3:
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0) emit('update', 'second', averageTotal.value, 'second')
return average01 + '/' + average02; break
}, case 4:
// checkbox if (checkboxList.value.length === 0) {
checkboxString: function () { checkboxList.value.push(checkCopy.value[0])
let str = this.checkboxList.join(); } else {
return str == '' ? '*' : str; checkCopy.value = checkboxList.value
} }
} emit('update', 'second', checkboxString.value, 'second')
break
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

@ -1,202 +1,197 @@
<template> <template>
<el-form size='small'> <el-form>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="1"> <el-radio v-model='radioValue' :label="1">
允许的通配符[, - * ? / L #] 允许的通配符[, - * ? / L #]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="2"> <el-radio v-model='radioValue' :label="2">
不指定 不指定
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="3"> <el-radio v-model='radioValue' :label="3">
周期从星期 周期从
<el-select clearable v-model="cycle01"> <el-select clearable v-model="cycle01">
<el-option <el-option
v-for="(item,index) of weekList" v-for="(item,index) of weekList"
:key="index" :key="index"
:label="item.value" :label="item.value"
:value="item.key" :value="item.key"
:disabled="item.key === 1" :disabled="item.key === 7"
>{{item.value}}</el-option> >{{item.value}}</el-option>
</el-select> </el-select>
- -
<el-select clearable v-model="cycle02"> <el-select clearable v-model="cycle02">
<el-option <el-option
v-for="(item,index) of weekList" v-for="(item,index) of weekList"
:key="index" :key="index"
:label="item.value" :label="item.value"
:value="item.key" :value="item.key"
:disabled="item.key < cycle01 && item.key !== 1" :disabled="item.key <= cycle01"
>{{item.value}}</el-option> >{{item.value}}</el-option>
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="4"> <el-radio v-model='radioValue' :label="4">
<el-input-number v-model='average01' :min="1" :max="4" /> 周的星期 <el-input-number v-model='average01' :min="1" :max="4" /> 周的
<el-select clearable v-model="average02"> <el-select clearable v-model="average02">
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option> <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="5"> <el-radio v-model='radioValue' :label="5">
本月最后一个星期 本月最后一个
<el-select clearable v-model="weekday"> <el-select clearable v-model="weekday">
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option> <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio v-model='radioValue' :label="6"> <el-radio v-model='radioValue' :label="6">
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%"> <el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="String(item.key)">{{item.value}}</el-option> <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(['update'])
data() { const props = defineProps({
return { cron: {
radioValue: 2, type: Object,
weekday: 2, default: {
cycle01: 2, second: "*",
cycle02: 3, min: "*",
average01: 1, hour: "*",
average02: 2, day: "*",
checkboxList: [], month: "*",
weekList: [ week: "?",
{ year: ""
key: 2, }
value: '星期一' },
}, check: {
{ type: Function,
key: 3, default: () => {
value: '星期二' }
}, }
{ })
key: 4, const radioValue = ref(2)
value: '星期三' const cycle01 = ref(2)
}, const cycle02 = ref(3)
{ const average01 = ref(1)
key: 5, const average02 = ref(2)
value: '星期四' const weekday = ref(2)
}, const checkboxList = ref([])
{ const checkCopy = ref([2])
key: 6, const weekList = ref([
value: '星期五' {key: 1, value: '星期日'},
}, {key: 2, value: '星期一'},
{ {key: 3, value: '星期二'},
key: 7, {key: 4, value: '星期三'},
value: '星期六' {key: 5, value: '星期四'},
}, {key: 6, value: '星期五'},
{ {key: 7, value: '星期六'}
key: 1, ])
value: '星期日' const cycleTotal = computed(() => {
} cycle01.value = props.check(cycle01.value, 1, 6)
], cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
checkNum: this.$options.propsData.check return cycle01.value + '-' + cycle02.value
} })
}, const averageTotal = computed(() => {
name: 'crontab-week', average01.value = props.check(average01.value, 1, 4)
props: ['check', 'cron'], average02.value = props.check(average02.value, 1, 7)
methods: { return average02.value + '#' + average01.value
// })
radioChange() { const weekdayTotal = computed(() => {
if (this.radioValue !== 2 && this.cron.day !== '?') { weekday.value = props.check(weekday.value, 1, 7)
this.$emit('update', 'day', '?', 'week'); return weekday.value + 'L'
} })
switch (this.radioValue) { const checkboxString = computed(() => {
case 1: return checkboxList.value.join(',')
this.$emit('update', 'week', '*'); })
break; watch(() => props.cron.week, value => changeRadioValue(value))
case 2: watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
this.$emit('update', 'week', '?'); function changeRadioValue(value) {
break; if (value === "*") {
case 3: radioValue.value = 1
this.$emit('update', 'week', this.cycleTotal); } else if (value === "?") {
break; radioValue.value = 2
case 4: } else if (value.indexOf("-") > -1) {
this.$emit('update', 'week', this.averageTotal); const indexArr = value.split('-')
break; cycle01.value = Number(indexArr[0])
case 5: cycle02.value = Number(indexArr[1])
this.$emit('update', 'week', this.weekdayCheck + 'L'); radioValue.value = 3
break; } else if (value.indexOf("#") > -1) {
case 6: const indexArr = value.split('#')
this.$emit('update', 'week', this.checkboxString); average01.value = Number(indexArr[1])
break; average02.value = Number(indexArr[0])
} radioValue.value = 4
}, } else if (value.indexOf("L") > -1) {
const indexArr = value.split("L")
// weekday.value = Number(indexArr[0])
cycleChange() { radioValue.value = 5
if (this.radioValue == '3') { } else {
this.$emit('update', 'week', this.cycleTotal); checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
} radioValue.value = 6
}, }
// }
averageChange() { function onRadioChange() {
if (this.radioValue == '4') { if (radioValue.value === 2 && props.cron.day === '?') {
this.$emit('update', 'week', this.averageTotal); emit('update', 'day', '*', 'week')
} }
}, if (radioValue.value !== 2 && props.cron.day !== '?') {
// emit('update', 'day', '?', 'week')
weekdayChange() { }
if (this.radioValue == '5') { switch (radioValue.value) {
this.$emit('update', 'week', this.weekday + 'L'); case 1:
} emit('update', 'week', '*', 'week')
}, break
// checkbox case 2:
checkboxChange() { emit('update', 'week', '?', 'week')
if (this.radioValue == '6') { break
this.$emit('update', 'week', this.checkboxString); case 3:
} emit('update', 'week', cycleTotal.value, 'week')
}, break
}, case 4:
watch: { emit('update', 'week', averageTotal.value, 'week')
'radioValue': 'radioChange', break
'cycleTotal': 'cycleChange', case 5:
'averageTotal': 'averageChange', emit('update', 'week', weekdayTotal.value, 'week')
'weekdayCheck': 'weekdayChange', break
'checkboxString': 'checkboxChange', case 6:
}, if (checkboxList.value.length === 0) {
computed: { checkboxList.value.push(checkCopy.value[0])
// } else {
cycleTotal: function () { checkCopy.value = checkboxList.value
this.cycle01 = this.checkNum(this.cycle01, 1, 7) }
this.cycle02 = this.checkNum(this.cycle02, 1, 7) emit('update', 'week', checkboxString.value, 'week')
return this.cycle01 + '-' + this.cycle02; break
}, }
//
averageTotal: function () {
this.average01 = this.checkNum(this.average01, 1, 4)
this.average02 = this.checkNum(this.average02, 1, 7)
return this.average02 + '#' + this.average01;
},
//
weekdayCheck: function () {
this.weekday = this.checkNum(this.weekday, 1, 7)
return this.weekday;
},
// checkbox
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
} }
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.5rem;
}
.el-select, .el-select--small {
width: 8rem;
}
.el-select.multiselect, .el-select--small.multiselect {
width: 17.8rem;
}
</style>

@ -1,131 +1,149 @@
<template> <template>
<el-form size="small"> <el-form>
<el-form-item> <el-form-item>
<el-radio :label="1" v-model='radioValue'> <el-radio :label="1" v-model='radioValue'>
不填允许的通配符[, - * /] 不填允许的通配符[, - * /]
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio :label="2" v-model='radioValue'> <el-radio :label="2" v-model='radioValue'>
每年 每年
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio :label="3" v-model='radioValue'> <el-radio :label="3" v-model='radioValue'>
周期从 周期从
<el-input-number v-model='cycle01' :min='fullYear' :max="2098" /> - <el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099" /> <el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio :label="4" v-model='radioValue'> <el-radio :label="4" v-model='radioValue'>
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始 <el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear" /> 年执行一次 <el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
</el-radio> </el-radio>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-radio :label="5" v-model='radioValue'> <el-radio :label="5" v-model='radioValue'>
指定 指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple> <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" /> <el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
</el-select> </el-select>
</el-radio> </el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(['update'])
data() { const props = defineProps({
return { cron: {
fullYear: 0, type: Object,
radioValue: 1, default: {
cycle01: 0, second: "*",
cycle02: 0, min: "*",
average01: 0, hour: "*",
average02: 1, day: "*",
checkboxList: [], month: "*",
checkNum: this.$options.propsData.check week: "?",
} year: ""
}, }
name: 'crontab-year', },
props: ['check', 'month', 'cron'], check: {
methods: { type: Function,
// default: () => {
radioChange() { }
switch (this.radioValue) { }
case 1: })
this.$emit('update', 'year', ''); const fullYear = ref(0)
break; const maxFullYear = ref(0)
case 2: const radioValue = ref(1)
this.$emit('update', 'year', '*'); const cycle01 = ref(0)
break; const cycle02 = ref(0)
case 3: const average01 = ref(0)
this.$emit('update', 'year', this.cycleTotal); const average02 = ref(1)
break; const checkboxList = ref([])
case 4: const checkCopy = ref([])
this.$emit('update', 'year', this.averageTotal); const cycleTotal = computed(() => {
break; cycle01.value = props.check(cycle01.value, fullYear.value, maxFullYear.value - 1)
case 5: cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear.value)
this.$emit('update', 'year', this.checkboxString); return cycle01.value + '-' + cycle02.value
break; })
} const averageTotal = computed(() => {
}, average01.value = props.check(average01.value, fullYear.value, maxFullYear.value - 1)
// average02.value = props.check(average02.value, 1, 10)
cycleChange() { return average01.value + '/' + average02.value
if (this.radioValue == '3') { })
this.$emit('update', 'year', this.cycleTotal); const checkboxString = computed(() => {
} return checkboxList.value.join(',')
}, })
// watch(() => props.cron.year, value => changeRadioValue(value))
averageChange() { watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
if (this.radioValue == '4') { function changeRadioValue(value) {
this.$emit('update', 'year', this.averageTotal); if (value === '') {
} radioValue.value = 1
}, } else if (value === "*") {
// checkbox radioValue.value = 2
checkboxChange() { } else if (value.indexOf("-") > -1) {
if (this.radioValue == '5') { const indexArr = value.split('-')
this.$emit('update', 'year', this.checkboxString); cycle01.value = Number(indexArr[0])
} cycle02.value = Number(indexArr[1])
} radioValue.value = 3
}, } else if (value.indexOf("/") > -1) {
watch: { const indexArr = value.split('/')
'radioValue': 'radioChange', average01.value = Number(indexArr[1])
'cycleTotal': 'cycleChange', average02.value = Number(indexArr[0])
'averageTotal': 'averageChange', radioValue.value = 4
'checkboxString': 'checkboxChange' } else {
}, checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
computed: { radioValue.value = 5
// }
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
}
} }
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'year', '', 'year')
break
case 2:
emit('update', 'year', '*', 'year')
break
case 3:
emit('update', 'year', cycleTotal.value, 'year')
break
case 4:
emit('update', 'year', averageTotal.value, 'year')
break
case 5:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'year', checkboxString.value, 'year')
break
}
}
onMounted(() => {
fullYear.value = Number(new Date().getFullYear())
maxFullYear.value = fullYear.value + 10
cycle01.value = fullYear.value
cycle02.value = cycle01.value + 1
average01.value = fullYear.value
checkCopy.value = [fullYear.value]
})
</script> </script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

@ -1,49 +0,0 @@
import Vue from 'vue'
import store from '@/store'
import DataDict from '@/utils/dict'
import { getDicts as getDicts } from '@/api/system/dict/data'
function searchDictByKey(dict, key) {
if (key == null && key == "") {
return null
}
try {
for (let i = 0; i < dict.length; i++) {
if (dict[i].key == key) {
return dict[i].value
}
}
} catch (e) {
return null
}
}
function install() {
Vue.use(DataDict, {
metas: {
'*': {
labelField: 'dictLabel',
valueField: 'dictValue',
request(dictMeta) {
const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
if (storeDict) {
return new Promise(resolve => { resolve(storeDict) })
} else {
return new Promise((resolve, reject) => {
getDicts(dictMeta.type).then(res => {
store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
resolve(res.data)
}).catch(error => {
reject(error)
})
})
}
},
},
},
})
}
export default {
install,
}

@ -3,22 +3,19 @@
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="values.includes(item.value)">
<span <span
v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)" v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
:key="item.value" :key="item.value"
:index="index" :index="index"
:class="item.raw.cssClass" :class="item.elTagClass"
>{{ item.label + ' ' }}</span >{{ item.label + " " }}</span>
>
<el-tag <el-tag
v-else v-else
:disable-transitions="true" :disable-transitions="true"
:key="item.value" :key="item.value + ''"
:index="index" :index="index"
:type="item.raw.listClass == 'primary' ? '' : item.raw.listClass" :type="item.elTagType === 'primary' ? '' : item.elTagType"
:class="item.raw.cssClass" :class="item.elTagClass"
> >{{ item.label + " " }}</el-tag>
{{ item.label + ' ' }}
</el-tag>
</template> </template>
</template> </template>
<template v-if="unmatch && showValue"> <template v-if="unmatch && showValue">
@ -27,61 +24,57 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { //
name: "DictTag", const unmatchArray = ref([]);
props: {
options: {
type: Array,
default: null,
},
value: [Number, String, Array],
// value
showValue: {
type: Boolean,
default: true,
},
separator: {
type: String,
default: ","
}
},
data() {
return {
unmatchArray: [], //
}
},
computed: {
values() {
if (this.value === null || typeof this.value === 'undefined' || this.value === '') return []
return Array.isArray(this.value) ? this.value.map(item => '' + item) : String(this.value).split(this.separator)
},
unmatch() {
this.unmatchArray = []
// value
if (this.value === null || typeof this.value === 'undefined' || this.value === '' || this.options.length === 0) return false
//
let unmatch = false //
this.values.forEach(item => {
if (!this.options.some(v => v.value === item)) {
this.unmatchArray.push(item)
unmatch = true // true
}
})
return unmatch //
},
const props = defineProps({
//
options: {
type: Array,
default: null,
},
//
value: [Number, String, Array],
// value
showValue: {
type: Boolean,
default: true,
}, },
filters: { separator: {
handleArray(array) { type: String,
if (array.length === 0) return ''; default: ",",
return array.reduce((pre, cur) => {
return pre + ' ' + cur;
})
},
} }
}; });
const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return [];
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
});
const unmatch = computed(() => {
unmatchArray.value = [];
// value
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || props.options.length === 0) return false
//
let unmatch = false //
values.value.forEach(item => {
if (!props.options.some(v => v.value === item)) {
unmatchArray.value.push(item)
unmatch = true // true
}
})
return unmatch //
});
function handleArray(array) {
if (array.length === 0) return "";
return array.reduce((pre, cur) => {
return pre + " " + cur;
});
}
</script> </script>
<style scoped> <style scoped>
.el-tag + .el-tag { .el-tag + .el-tag {
margin-left: 10px; margin-left: 10px;

@ -8,195 +8,172 @@
name="file" name="file"
:show-file-list="false" :show-file-list="false"
:headers="headers" :headers="headers"
style="display: none" class="editor-img-uploader"
ref="upload" v-if="type == 'url'"
v-if="this.type == 'url'"
> >
<i ref="uploadRef" class="editor-img-uploader"></i>
</el-upload> </el-upload>
<div class="editor" ref="editor" :style="styles"></div> </div>
<div class="editor">
<quill-editor
ref="quillEditorRef"
v-model:content="content"
contentType="html"
@textChange="(e) => $emit('update:modelValue', content)"
:options="options"
:style="styles"
/>
</div> </div>
</template> </template>
<script> <script setup>
import Quill from "quill"; import { QuillEditor } from "@vueup/vue-quill";
import "quill/dist/quill.core.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { const { proxy } = getCurrentInstance();
name: "Editor",
props: { const quillEditorRef = ref();
/* 编辑器的内容 */ const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); //
value: { const headers = ref({
type: String, Authorization: "Bearer " + getToken()
default: "", });
},
/* 高度 */ const props = defineProps({
height: { /* 编辑器的内容 */
type: Number, modelValue: {
default: null, type: String,
},
/* 最小高度 */
minHeight: {
type: Number,
default: null,
},
/* 只读 */
readOnly: {
type: Boolean,
default: false,
},
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
},
/* 类型base64格式、url格式 */
type: {
type: String,
default: "url",
}
}, },
data() { /* 高度 */
return { height: {
uploadUrl: process.env.VUE_APP_BASE_API + "/file/upload", // type: Number,
headers: { default: null,
Authorization: "Bearer " + getToken()
},
Quill: null,
currentValue: "",
options: {
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
//
toolbar: [
["bold", "italic", "underline", "strike"], // 线 线
["blockquote", "code-block"], //
[{ list: "ordered" }, { list: "bullet" }], //
[{ indent: "-1" }, { indent: "+1" }], //
[{ size: ["small", false, "large", "huge"] }], //
[{ header: [1, 2, 3, 4, 5, 6, false] }], //
[{ color: [] }, { background: [] }], //
[{ align: [] }], //
["clean"], //
["link", "image", "video"] //
],
},
placeholder: "请输入内容",
readOnly: this.readOnly,
},
};
}, },
computed: { /* 最小高度 */
styles() { minHeight: {
let style = {}; type: Number,
if (this.minHeight) { default: null,
style.minHeight = `${this.minHeight}px`;
}
if (this.height) {
style.height = `${this.height}px`;
}
return style;
},
}, },
watch: { /* 只读 */
value: { readOnly: {
handler(val) { type: Boolean,
if (val !== this.currentValue) { default: false,
this.currentValue = val === null ? "" : val;
if (this.Quill) {
this.Quill.pasteHTML(this.currentValue);
}
}
},
immediate: true,
},
}, },
mounted() { /* 上传文件大小限制(MB) */
this.init(); fileSize: {
type: Number,
default: 5,
}, },
beforeDestroy() { /* 类型base64格式、url格式 */
this.Quill = null; type: {
type: String,
default: "base64",
}
});
const options = ref({
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
//
toolbar: [
["bold", "italic", "underline", "strike"], // 线 线
["blockquote", "code-block"], //
[{ list: "ordered" }, { list: "bullet" }], //
[{ indent: "-1" }, { indent: "+1" }], //
[{ size: ["small", false, "large", "huge"] }], //
[{ header: [1, 2, 3, 4, 5, 6, false] }], //
[{ color: [] }, { background: [] }], //
[{ align: [] }], //
["clean"], //
["link", "image", "video"] //
],
}, },
methods: { placeholder: "请输入内容",
init() { readOnly: props.readOnly
const editor = this.$refs.editor; });
this.Quill = new Quill(editor, this.options);
// const styles = computed(() => {
if (this.type == 'url') { let style = {};
let toolbar = this.Quill.getModule("toolbar"); if (props.minHeight) {
toolbar.addHandler("image", (value) => { style.minHeight = `${props.minHeight}px`;
if (value) { }
this.$refs.upload.$children[0].$refs.input.click(); if (props.height) {
} else { style.height = `${props.height}px`;
this.quill.format("image", false); }
} return style;
}); });
}
this.Quill.pasteHTML(this.currentValue); const content = ref("");
this.Quill.on("text-change", (delta, oldDelta, source) => { watch(() => props.modelValue, (v) => {
const html = this.$refs.editor.children[0].innerHTML; if (v !== content.value) {
const text = this.Quill.getText(); content.value = v === undefined ? "<p></p>" : v;
const quill = this.Quill; }
this.currentValue = html; }, { immediate: true });
this.$emit("input", html);
this.$emit("on-change", { html, text, quill }); //
}); onMounted(() => {
this.Quill.on("text-change", (delta, oldDelta, source) => { if (props.type == 'url') {
this.$emit("on-text-change", delta, oldDelta, source); let quill = quillEditorRef.value.getQuill();
}); let toolbar = quill.getModule("toolbar");
this.Quill.on("selection-change", (range, oldRange, source) => { toolbar.addHandler("image", (value) => {
this.$emit("on-selection-change", range, oldRange, source); if (value) {
}); proxy.$refs.uploadRef.click();
this.Quill.on("editor-change", (eventName, ...args) => {
this.$emit("on-editor-change", eventName, ...args);
});
},
//
handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
const isJPG = type.includes(file.type);
//
if (!isJPG) {
this.$message.error(`图片格式错误!`);
return false;
}
//
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
return true;
},
handleUploadSuccess(res, file) {
//
if (res.code == 200) {
//
let quill = this.Quill;
//
let length = quill.getSelection().index;
// res.url
quill.insertEmbed(length, "image", res.data.url);
//
quill.setSelection(length + 1);
} else { } else {
this.$message.error("图片插入失败"); quill.format("image", false);
} }
}, });
handleUploadError() { }
this.$message.error("图片插入失败"); });
},
}, //
}; function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
const isJPG = type.includes(file.type);
//
if (!isJPG) {
proxy.$modal.msgError(`图片格式错误!`);
return false;
}
//
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
return true;
}
//
function handleUploadSuccess(res, file) {
//
if (res.code == 200) {
//
let quill = toRaw(quillEditorRef.value).getQuill();
//
let length = quill.selection.savedRange.index;
// res.url
quill.insertEmbed(length, "image", res.data.url);
//
quill.setSelection(length + 1);
} else {
proxy.$modal.msgError("图片插入失败");
}
}
//
function handleUploadError() {
proxy.$modal.msgError("图片插入失败");
}
</script> </script>
<style> <style>
.editor-img-uploader {
display: none;
}
.editor, .ql-toolbar { .editor, .ql-toolbar {
white-space: pre-wrap !important; white-space: pre-wrap !important;
line-height: normal !important; line-height: normal !important;

@ -15,19 +15,18 @@
ref="fileUpload" ref="fileUpload"
> >
<!-- 上传按钮 --> <!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button> <el-button type="primary">选取文件</el-button>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
</el-upload> </el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
请上传
<template v-if="fileSize"> <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 --> <!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList"> <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="file.url" :underline="false" target="_blank"> <el-link :href="file.url" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span> <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link> </el-link>
@ -39,158 +38,150 @@
</div> </div>
</template> </template>
<script> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { const props = defineProps({
name: "FileUpload", modelValue: [String, Object, Array],
props: { //
// limit: {
value: [String, Object, Array], type: Number,
// default: 5,
limit: {
type: Number,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
},
//
isShowTip: {
type: Boolean,
default: true
}
}, },
data() { // (MB)
return { fileSize: {
number: 0, type: Number,
uploadList: [], default: 5,
uploadFileUrl: process.env.VUE_APP_BASE_API + "/file/upload", //
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: [],
};
}, },
watch: { // , ['png', 'jpg', 'jpeg']
value: { fileType: {
handler(val) { type: Array,
if (val) { default: () => ["doc", "xls", "ppt", "txt", "pdf"],
let temp = 1;
//
const list = Array.isArray(val) ? val : this.value.split(',');
//
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
}, },
computed: { //
// isShowTip: {
showTip() { type: Boolean,
return this.isShowTip && (this.fileType || this.fileSize); default: true
}, }
}, });
methods: {
// const { proxy } = getCurrentInstance();
handleBeforeUpload(file) { const emit = defineEmits();
// const number = ref(0);
if (this.fileType) { const uploadList = ref([]);
const fileName = file.name.split('.'); const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); //
const fileExt = fileName[fileName.length - 1]; const headers = ref({ Authorization: "Bearer " + getToken() });
const isTypeOk = this.fileType.indexOf(fileExt) >= 0; const fileList = ref([]);
if (!isTypeOk) { const showTip = computed(
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`); () => props.isShowTip && (props.fileType || props.fileSize)
return false; );
}
} watch(() => props.modelValue, val => {
// if (val) {
if (this.fileSize) { let temp = 1;
const isLt = file.size / 1024 / 1024 < this.fileSize; //
if (!isLt) { const list = Array.isArray(val) ? val : props.modelValue.split(',');
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`); //
return false; fileList.value = list.map(item => {
} if (typeof item === "string") {
} item = { name: item, url: item };
this.$modal.loading("正在上传文件,请稍候...");
this.number++;
return true;
},
//
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
//
handleUploadError(err) {
this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading()
},
//
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.data.url, url: res.data.url });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.fileUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
//
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", this.listToString(this.fileList));
},
//
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
//
getFileName(name) {
// url
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
},
//
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
} }
return strs != '' ? strs.substr(0, strs.length - 1) : ''; item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
},{ deep: true, immediate: true });
//
function handleBeforeUpload(file) {
//
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
return false;
}
}
//
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
}
//
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
//
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
}
//
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.data.url, url: res.data.url });
uploadedSuccessfully();
} else {
number.value--;
proxy.$modal.closeLoading();
proxy.$modal.msgError(res.msg);
proxy.$refs.fileUpload.handleRemove(file);
uploadedSuccessfully();
}
}
//
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
//
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
}
}
//
function getFileName(name) {
// url
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
}
//
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url + separator;
} }
} }
}; return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

@ -13,20 +13,17 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { defineProps({
name: 'Hamburger', isActive: {
props: { type: Boolean,
isActive: { default: false
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
} }
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick');
} }
</script> </script>

@ -1,8 +1,8 @@
<template> <template>
<div :class="{'show':show}" class="header-search"> <div :class="{ 'show': show }" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" /> <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select <el-select
ref="headerSearchSelect" ref="headerSearchSelectRef"
v-model="search" v-model="search"
:remote-method="querySearch" :remote-method="querySearch"
filterable filterable
@ -17,147 +17,136 @@
</div> </div>
</template> </template>
<script> <script setup>
// fuse is a lightweight fuzzy-search module import Fuse from 'fuse.js'
// make search results more in line with expectations import { getNormalPath } from '@/utils/ruoyi'
import Fuse from 'fuse.js/dist/fuse.min.js' import { isHttp } from '@/utils/validate'
import path from 'path' import usePermissionStore from '@/store/modules/permission'
export default { const search = ref('');
name: 'HeaderSearch', const options = ref([]);
data() { const searchPool = ref([]);
return { const show = ref(false);
search: '', const fuse = ref(undefined);
options: [], const headerSearchSelectRef = ref(null);
searchPool: [], const router = useRouter();
show: false, const routes = computed(() => usePermissionStore().routes);
fuse: undefined
function click() {
show.value = !show.value
if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
}
};
function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
options.value = []
show.value = false
}
function change(val) {
const path = val.path;
const query = val.query;
if (isHttp(path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
if (query) {
router.push({ path: path, query: JSON.parse(query) });
} else {
router.push(path)
} }
}, }
computed: {
routes() { search.value = ''
return this.$store.getters.permission_routes options.value = []
nextTick(() => {
show.value = false
})
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
let res = []
for (const r of routes) {
// skip hidden router
if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle]
} }
},
watch: { if (r.meta && r.meta.title) {
routes() { data.title = [...data.title, r.meta.title]
this.searchPool = this.generateRoutes(this.routes)
}, if (r.redirect !== 'noRedirect') {
searchPool(list) { // only push the routes with title
this.initFuse(list) // special case: need to exclude parent router without redirect
}, res.push(data)
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
} }
} }
}, if (r.query) {
mounted() { data.query = r.query
this.searchPool = this.generateRoutes(this.routes) }
},
methods: { // recursive child routes
click() { if (r.children) {
this.show = !this.show const tempRoutes = generateRoutes(r.children, data.path, data.title)
if (this.show) { if (tempRoutes.length >= 1) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus() res = [...res, ...tempRoutes]
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
const path = val.path;
const query = val.query;
if(this.ishttp(val.path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
if (query) {
this.$router.push({ path: path, query: JSON.parse(query) });
} else {
this.$router.push(path)
}
}
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
if (router.query) {
data.query = router.query
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
} }
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
} }
} }
return res
}
function querySearch(query) {
if (query !== '') {
options.value = fuse.value.search(query)
} else {
options.value = []
}
} }
onMounted(() => {
searchPool.value = generateRoutes(routes.value);
})
watchEffect(() => {
searchPool.value = generateRoutes(routes.value)
})
watch(show, (value) => {
if (value) {
document.body.addEventListener('click', close)
} else {
document.body.removeEventListener('click', close)
}
})
watch(searchPool, (list) => {
initFuse(list)
})
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.header-search { .header-search {
font-size: 0 !important; font-size: 0 !important;
@ -177,7 +166,7 @@ export default {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
::v-deep .el-input__inner { :deep(.el-input__inner) {
border-radius: 0; border-radius: 0;
border: 0; border: 0;
padding-left: 0; padding-left: 0;

@ -1,8 +1,14 @@
<!-- @author zhengjie -->
<template> <template>
<div class="icon-body"> <div class="icon-body">
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons"> <el-input
<i slot="suffix" class="el-icon-search el-input__icon" /> v-model="iconName"
class="icon-search"
clearable
placeholder="请输入图标名称"
@clear="filterIcons"
@input="filterIcons"
>
<template #suffix><i class="el-icon-search el-input__icon" /></template>
</el-input> </el-input>
<div class="icon-list"> <div class="icon-list">
<div class="list-container"> <div class="list-container">
@ -17,42 +23,43 @@
</div> </div>
</template> </template>
<script> <script setup>
import icons from './requireIcons' import icons from './requireIcons'
export default {
name: 'IconSelect', const props = defineProps({
props: { activeIcon: {
activeIcon: { type: String
type: String
}
},
data() {
return {
name: '',
iconList: icons
}
},
methods: {
filterIcons() {
this.iconList = icons
if (this.name) {
this.iconList = this.iconList.filter(item => item.includes(this.name))
}
},
selectedIcon(name) {
this.$emit('selected', name)
document.body.click()
},
reset() {
this.name = ''
this.iconList = icons
}
} }
});
const iconName = ref('');
const iconList = ref(icons);
const emit = defineEmits(['selected']);
function filterIcons() {
iconList.value = icons
if (iconName.value) {
iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
}
}
function selectedIcon(name) {
emit('selected', name)
document.body.click()
} }
function reset() {
iconName.value = ''
iconList.value = icons
}
defineExpose({
reset
})
</script> </script>
<style rel="stylesheet/scss" lang="scss" scoped> <style lang='scss' scoped>
.icon-body { .icon-body {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
.icon-search { .icon-search {

@ -1,11 +1,8 @@
let icons = []
const req = require.context('../../assets/icons/svg', false, /\.svg$/) const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
const requireAll = requireContext => requireContext.keys() for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
const re = /\.\/(.*)\.svg/ icons.push(p);
}
const icons = requireAll(req).map(i => {
return i.match(re)[1]
})
export default icons export default icons

@ -4,57 +4,59 @@
fit="cover" fit="cover"
:style="`width:${realWidth};height:${realHeight};`" :style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList" :preview-src-list="realSrcList"
preview-teleported
> >
<div slot="error" class="image-slot"> <template #error>
<i class="el-icon-picture-outline"></i> <div class="image-slot">
</div> <el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image> </el-image>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
name: "ImagePreview", src: {
props: { type: String,
src: { default: ""
type: String,
default: ""
},
width: {
type: [Number, String],
default: ""
},
height: {
type: [Number, String],
default: ""
}
}, },
computed: { width: {
realSrc() { type: [Number, String],
if (!this.src) { default: ""
return;
}
let real_src = this.src.split(",")[0];
return real_src;
},
realSrcList() {
if (!this.src) {
return;
}
let real_src_list = this.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
return srcList.push(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`;
}
}, },
}; height: {
type: [Number, String],
default: ""
}
});
const realSrc = computed(() => {
if (!props.src) {
return;
}
let real_src = props.src.split(",")[0];
return real_src;
});
const realSrcList = computed(() => {
if (!props.src) {
return;
}
let real_src_list = props.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
return srcList.push(item);
});
return srcList;
});
const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
);
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -62,14 +64,14 @@ export default {
border-radius: 5px; border-radius: 5px;
background-color: #ebeef5; background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc; box-shadow: 0 0 5px 1px #ccc;
::v-deep .el-image__inner { :deep(.el-image__inner) {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
transform: scale(1.2); transform: scale(1.2);
} }
} }
::v-deep .image-slot { :deep(.image-slot) {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

@ -10,28 +10,31 @@
:on-error="handleUploadError" :on-error="handleUploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
ref="imageUpload" ref="imageUpload"
:on-remove="handleDelete" :before-remove="handleDelete"
:show-file-list="true" :show-file-list="true"
:headers="headers" :headers="headers"
:file-list="fileList" :file-list="fileList"
:on-preview="handlePictureCardPreview" :on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}" :class="{ hide: fileList.length >= limit }"
> >
<i class="el-icon-plus"></i> <el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload> </el-upload>
<!-- 上传提示 --> <!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip"> <div class="el-upload__tip" v-if="showTip">
请上传 请上传
<template v-if="fileSize"> <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> <template v-if="fileSize">
<template v-if="fileType"> <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件 的文件
</div> </div>
<el-dialog <el-dialog
:visible.sync="dialogVisible" v-model="dialogVisible"
title="预览" title="预览"
width="800" width="800px"
append-to-body append-to-body
> >
<img <img
@ -42,180 +45,164 @@
</div> </div>
</template> </template>
<script> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { const props = defineProps({
props: { modelValue: [String, Object, Array],
value: [String, Object, Array], //
// limit: {
limit: { type: Number,
type: Number, default: 5,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
},
//
isShowTip: {
type: Boolean,
default: true
}
}, },
data() { // (MB)
return { fileSize: {
number: 0, type: Number,
uploadList: [], default: 5,
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/file/upload", //
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
};
}, },
watch: { // , ['png', 'jpg', 'jpeg']
value: { fileType: {
handler(val) { type: Array,
if (val) { default: () => ["png", "jpg", "jpeg"],
//
const list = Array.isArray(val) ? val : this.value.split(',');
//
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
}, },
computed: { //
// isShowTip: {
showTip() { type: Boolean,
return this.isShowTip && (this.fileType || this.fileSize); default: true
},
}, },
methods: { });
// loading
handleBeforeUpload(file) {
let isImg = false;
if (this.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isImg = this.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) { const { proxy } = getCurrentInstance();
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`); const emit = defineEmits();
return false; const number = ref(0);
} const uploadList = ref([]);
if (this.fileSize) { const dialogImageUrl = ref("");
const isLt = file.size / 1024 / 1024 < this.fileSize; const dialogVisible = ref(false);
if (!isLt) { const baseUrl = import.meta.env.VITE_APP_BASE_API;
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`); const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); //
return false; const headers = ref({ Authorization: "Bearer " + getToken() });
} const fileList = ref([]);
} const showTip = computed(
this.$modal.loading("正在上传图片,请稍候..."); () => props.isShowTip && (props.fileType || props.fileSize)
this.number++; );
},
// watch(() => props.modelValue, val => {
handleExceed() { if (val) {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`); //
}, const list = Array.isArray(val) ? val : props.modelValue.split(",");
// //
handleUploadSuccess(res, file) { fileList.value = list.map(item => {
if (res.code === 200) { if (typeof item === "string") {
this.uploadList.push({ name: res.data.url, url: res.data.url }); item = { name: item, url: item };
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.imageUpload.handleRemove(file);
this.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));
}
},
//
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();
}
},
//
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
//
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator;
}
} }
return strs != '' ? strs.substr(0, strs.length - 1) : ''; return item;
});
} else {
fileList.value = [];
return [];
}
},{ deep: true, immediate: true });
// loading
function handleBeforeUpload(file) {
let isImg = false;
if (props.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) {
proxy.$modal.msgError(
`文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
);
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
} }
} }
}; proxy.$modal.loading("正在上传图片,请稍候...");
number.value++;
}
//
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
//
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.data.url, url: res.data.url });
uploadedSuccessfully();
} else {
number.value--;
proxy.$modal.closeLoading();
proxy.$modal.msgError(res.msg);
proxy.$refs.imageUpload.handleRemove(file);
uploadedSuccessfully();
}
}
//
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name);
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1);
emit("update:modelValue", listToString(fileList.value));
return false;
}
}
//
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
}
}
//
function handleUploadError() {
proxy.$modal.msgError("上传图片失败");
proxy.$modal.closeLoading();
}
//
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
}
//
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator;
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// .el-upload--picture-card // .el-upload--picture-card
::v-deep.hide .el-upload--picture-card { :deep(.hide .el-upload--picture-card) {
display: none; display: none;
} }
//
::v-deep .el-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> </style>

@ -1,106 +1,97 @@
<template> <template>
<div :class="{'hidden':hidden}" class="pagination-container"> <div :class="{ 'hidden': hidden }" class="pagination-container">
<el-pagination <el-pagination
:background="background" :background="background"
:current-page.sync="currentPage" v-model:current-page="currentPage"
:page-size.sync="pageSize" v-model:page-size="pageSize"
:layout="layout" :layout="layout"
:page-sizes="pageSizes" :page-sizes="pageSizes"
:pager-count="pagerCount" :pager-count="pagerCount"
:total="total" :total="total"
v-bind="$attrs"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
/> />
</div> </div>
</template> </template>
<script> <script setup>
import { scrollTo } from '@/utils/scroll-to' import { scrollTo } from '@/utils/scroll-to'
export default { const props = defineProps({
name: 'Pagination', total: {
props: { required: true,
total: { type: Number
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
}, },
data() { page: {
return { type: Number,
}; default: 1
}, },
computed: { limit: {
currentPage: { type: Number,
get() { default: 20
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
}, },
methods: { pageSizes: {
handleSizeChange(val) { type: Array,
if (this.currentPage * val > this.total) { default() {
this.currentPage = 1 return [10, 20, 30, 50]
}
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) {
scrollTo(0, 800)
}
} }
},
// 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
})
const emit = defineEmits();
const currentPage = computed({
get() {
return props.page
},
set(val) {
emit('update:page', val)
}
})
const pageSize = computed({
get() {
return props.limit
},
set(val){
emit('update:limit', val)
}
})
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1
}
emit('pagination', { page: currentPage.value, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
} }
} }
</script> </script>
<style scoped> <style scoped>

@ -1,142 +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>
<!-- eslint-disable-next-line -->
<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,106 +0,0 @@
<template>
<div ref="rightPanel" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
}
},
computed: {
show: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
}
},
mounted() {
this.addEventClick()
},
beforeDestroy() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.el-drawer__body')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
}
}
}
</script>
<style lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>

@ -2,26 +2,28 @@
<div class="top-right-btn" :style="style"> <div class="top-right-btn" :style="style">
<el-row> <el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search"> <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" /> <el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top"> <el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" /> <el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns"> <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
<el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/> <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'"> <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button size="mini" circle icon="el-icon-menu" /> <el-button circle icon="Menu" />
<el-dropdown-menu slot="dropdown"> <template #dropdown>
<template v-for="item in columns"> <el-dropdown-menu>
<el-dropdown-item :key="item.key"> <template v-for="item in columns" :key="item.key">
<el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" /> <el-dropdown-item>
</el-dropdown-item> <el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
</template> </el-dropdown-item>
</el-dropdown-menu> </template>
</el-dropdown-menu>
</template>
</el-dropdown> </el-dropdown>
</el-tooltip> </el-tooltip>
</el-row> </el-row>
<el-dialog :title="title" :visible.sync="open" append-to-body> <el-dialog :title="title" v-model="open" append-to-body>
<el-transfer <el-transfer
:titles="['显示', '隐藏']" :titles="['显示', '隐藏']"
v-model="value" v-model="value"
@ -31,99 +33,102 @@
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script>
export default { <script setup>
name: "RightToolbar", const props = defineProps({
data() { /* 是否显示检索条件 */
return { showSearch: {
// type: Boolean,
value: [], default: true,
//
title: "显示/隐藏",
//
open: false,
};
}, },
props: { /* 显隐列信息 */
/* 是否显示检索条件 */ columns: {
showSearch: { type: Array,
type: Boolean,
default: true,
},
/* 显隐列信息 */
columns: {
type: Array,
},
/* 是否显示检索图标 */
search: {
type: Boolean,
default: true,
},
/* 显隐列类型transfer穿梭框、checkbox复选框 */
showColumnsType: {
type: String,
default: "checkbox",
},
/* 右外边距 */
gutter: {
type: Number,
default: 10,
},
}, },
computed: { /* 是否显示检索图标 */
style() { search: {
const ret = {}; type: Boolean,
if (this.gutter) { default: true,
ret.marginRight = `${this.gutter / 2}px`;
}
return ret;
}
}, },
created() { /* 显隐列类型transfer穿梭框、checkbox复选框 */
if (this.showColumnsType == 'transfer') { showColumnsType: {
// type: String,
for (let item in this.columns) { default: "checkbox",
if (this.columns[item].visible === false) {
this.value.push(parseInt(item));
}
}
}
}, },
methods: { /* 右外边距 */
// gutter: {
toggleSearch() { type: Number,
this.$emit("update:showSearch", !this.showSearch); default: 10,
},
//
refresh() {
this.$emit("queryTable");
},
//
dataChange(data) {
for (let item in this.columns) {
const key = this.columns[item].key;
this.columns[item].visible = !data.includes(key);
}
},
// dialog
showColumn() {
this.open = true;
},
//
checkboxChange(event, label) {
this.columns.filter(item => item.label == label)[0].visible = event;
}
}, },
}; })
const emits = defineEmits(['update:showSearch', 'queryTable']);
//
const value = ref([]);
//
const title = ref("显示/隐藏");
//
const open = ref(false);
const style = computed(() => {
const ret = {};
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`;
}
return ret;
});
//
function toggleSearch() {
emits("update:showSearch", !props.showSearch);
}
//
function refresh() {
emits("queryTable");
}
//
function dataChange(data) {
for (let item in props.columns) {
const key = props.columns[item].key;
props.columns[item].visible = !data.includes(key);
}
}
// dialog
function showColumn() {
open.value = true;
}
if (props.showColumnsType == 'transfer') {
//
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item));
}
}
}
//
function checkboxChange(event, label) {
props.columns.filter(item => item.label == label)[0].visible = event;
}
</script> </script>
<style lang="scss" scoped>
::v-deep .el-transfer__button { <style lang='scss' scoped>
:deep(.el-transfer__button) {
border-radius: 50%; border-radius: 50%;
padding: 12px;
display: block; display: block;
margin-left: 0px; margin-left: 0px;
} }
::v-deep .el-transfer__button:first-child { :deep(.el-transfer__button:first-child) {
margin-bottom: 10px; margin-bottom: 10px;
} }
:deep(.el-dropdown-menu__item) {
line-height: 30px;
padding: 0 17px;
}
</style> </style>

@ -4,18 +4,10 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const url = ref('http://doc.ruoyi.vip/ruoyi-cloud');
name: 'RuoYiDoc',
data() { function goto() {
return { window.open(url.value)
url: 'http://doc.ruoyi.vip/ruoyi-cloud'
}
},
methods: {
goto() {
window.open(this.url)
}
}
} }
</script> </script>

@ -4,18 +4,10 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const url = ref('https://gitee.com/y_project/RuoYi-Cloud');
name: 'RuoYiGit',
data() { function goto() {
return { window.open(url.value)
url: 'https://gitee.com/y_project/RuoYi-Cloud'
}
},
methods: {
goto() {
window.open(this.url)
}
}
} }
</script> </script>

@ -1,55 +1,20 @@
<template> <template>
<div> <div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" /> <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div> </div>
</template> </template>
<script> <script setup>
import screenfull from 'screenfull' import { useFullscreen } from '@vueuse/core'
export default { const { isFullscreen, enter, exit, toggle } = useFullscreen();
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.isEnabled) {
this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.isEnabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.isEnabled) {
screenfull.off('change', this.change)
}
}
}
}
</script> </script>
<style scoped> <style lang='scss' scoped>
.screenfull-svg { .screenfull-svg {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
fill: #5a5e66;; fill: #5a5e66;
width: 20px; width: 20px;
height: 20px; height: 20px;
vertical-align: 10px; vertical-align: 10px;

@ -1,56 +1,45 @@
<template> <template>
<el-dropdown trigger="click" @command="handleSetSize"> <div>
<div> <el-dropdown trigger="click" @command="handleSetSize">
<svg-icon class-name="size-icon" icon-class="size" /> <div class="size-icon--style">
</div> <svg-icon class-name="size-icon" icon-class="size" />
<el-dropdown-menu slot="dropdown"> </div>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value"> <template #dropdown>
{{ item.label }} <el-dropdown-menu>
</el-dropdown-item> <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
</el-dropdown-menu> {{ item.label }}
</el-dropdown> </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template> </template>
<script> <script setup>
export default { import useAppStore from "@/store/modules/app";
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('app/setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route const appStore = useAppStore();
const size = computed(() => appStore.size);
this.$nextTick(() => { const route = useRoute();
this.$router.replace({ const router = useRouter();
path: '/redirect' + fullPath const { proxy } = getCurrentInstance();
}) const sizeOptions = ref([
}) { label: "较大", value: "large" },
} { label: "默认", value: "default" },
} { label: "稍小", value: "small" },
]);
function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候...");
appStore.setSize(size);
setTimeout("window.location.reload()", 1000);
} }
</script> </script>
<style lang='scss' scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>

@ -1,15 +1,11 @@
<template> <template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" /> <svg :class="svgClass" aria-hidden="true">
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners"> <use :xlink:href="iconName" :fill="color" />
<use :xlink:href="iconName" />
</svg> </svg>
</template> </template>
<script> <script>
import { isExternal } from '@/utils/validate' export default defineComponent({
export default {
name: 'SvgIcon',
props: { props: {
iconClass: { iconClass: {
type: String, type: String,
@ -18,44 +14,40 @@ export default {
className: { className: {
type: String, type: String,
default: '' default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
}, },
iconName() { color: {
return `#icon-${this.iconClass}` type: String,
default: ''
}, },
svgClass() { },
if (this.className) { setup(props) {
return 'svg-icon ' + this.className return {
} else { iconName: computed(() => `#icon-${props.iconClass}`),
svgClass: computed(() => {
if (props.className) {
return `svg-icon ${props.className}`
}
return 'svg-icon' return 'svg-icon'
} })
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
} }
} }
} })
</script> </script>
<style scoped> <style scope lang="scss">
.sub-el-icon,
.nav-icon {
display: inline-block;
font-size: 15px;
margin-right: 12px;
position: relative;
}
.svg-icon { .svg-icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
vertical-align: -0.15em; position: relative;
fill: currentColor; fill: currentColor;
overflow: hidden; vertical-align: -2px;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
} }
</style> </style>

@ -0,0 +1,10 @@
import * as components from '@element-plus/icons-vue'
export default {
install: (app) => {
for (const key in components) {
const componentConfig = components[key];
app.component(componentConfig.name, componentConfig);
}
},
};

@ -1,173 +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 version = require('element-ui/package.json').version // element-ui version from node_modules
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 = `https://unpkg.com/element-ui@${version}/lib/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>

@ -3,6 +3,7 @@
:default-active="activeMenu" :default-active="activeMenu"
mode="horizontal" mode="horizontal"
@select="handleSelect" @select="handleSelect"
:ellipsis="false"
> >
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"> <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
@ -14,158 +15,158 @@
</template> </template>
<!-- 顶部菜单超出数量折叠 --> <!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber"> <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template> <template #title>更多菜单</template>
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item <el-menu-item
:index="item.path" :index="item.path"
:key="index" :key="index"
v-if="index >= visibleNumber"> v-if="index >= visibleNumber">
<svg-icon <svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/> :icon-class="item.meta.icon"/>
{{ item.meta.title }} {{ item.meta.title }}
</el-menu-item> </el-menu-item>
</template> </template>
</el-submenu> </el-sub-menu>
</el-menu> </el-menu>
</template> </template>
<script> <script setup>
import { constantRoutes } from "@/router"; import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
//
const visibleNumber = ref(null);
// index
const currentIndex = ref(null);
// //
const hideList = ['/index', '/user/profile']; const hideList = ['/index', '/user/profile'];
export default { const appStore = useAppStore()
data() { const settingsStore = useSettingsStore()
return { const permissionStore = usePermissionStore()
// const route = useRoute();
visibleNumber: 5, const router = useRouter();
// index
currentIndex: undefined //
}; const theme = computed(() => settingsStore.theme);
}, //
computed: { const routers = computed(() => permissionStore.topbarRouters);
theme() {
return this.$store.state.settings.theme; //
}, const topMenus = computed(() => {
// let topMenus = [];
topMenus() { routers.value.map((menu) => {
let topMenus = []; if (menu.hidden !== true) {
this.routers.map((menu) => { //
if (menu.hidden !== true) { if (menu.path === "/") {
// topMenus.push(menu.children[0]);
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
}
}
});
return topMenus;
},
//
routers() {
return this.$store.state.permission.topbarRouters;
},
//
childrenMenus() {
var childrenMenus = [];
this.routers.map((router) => {
for (var item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!this.ishttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
});
return constantRoutes.concat(childrenMenus);
},
//
activeMenu() {
const path = this.$route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!this.$route.meta.link) {
this.$store.dispatch('app/toggleSideBarHide', false);
}
} else if(!this.$route.children) {
activePath = path;
this.$store.dispatch('app/toggleSideBarHide', true);
}
this.activeRoutes(activePath);
return activePath;
},
},
beforeMount() {
window.addEventListener('resize', this.setVisibleNumber)
},
beforeDestroy() {
window.removeEventListener('resize', this.setVisibleNumber)
},
mounted() {
this.setVisibleNumber();
},
methods: {
//
setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
this.visibleNumber = parseInt(width / 85);
},
//
handleSelect(key, keyPath) {
this.currentIndex = key;
const route = this.routers.find(item => item.path === key);
if (this.ishttp(key)) {
// http(s)://
window.open(key, "_blank");
} else if (!route || !route.children) {
//
const routeMenu = this.childrenMenus.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
this.$router.push({ path: key, query: query });
} else {
this.$router.push({ path: key });
}
this.$store.dispatch('app/toggleSideBarHide', true);
} else { } else {
// topMenus.push(menu);
this.activeRoutes(key);
this.$store.dispatch('app/toggleSideBarHide', false);
} }
}, }
// })
activeRoutes(key) { return topMenus;
var routes = []; })
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => { //
if (key == item.parentPath || (key == "index" && "" == item.path)) { const childrenMenus = computed(() => {
routes.push(item); 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;
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
} }
}); }
} router.children[item].parentPath = router.path;
if(routes.length > 0) {
this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
} else {
this.$store.dispatch('app/toggleSideBarHide', true);
} }
}, childrenMenus.push(router.children[item]);
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
} }
}, })
}; return constantRoutes.concat(childrenMenus);
})
//
const activeMenu = computed(() => {
const path = route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!route.meta.link) {
appStore.toggleSideBarHide(false);
}
} else if(!route.children) {
activePath = path;
appStore.toggleSideBarHide(true);
}
activeRoutes(activePath);
return activePath;
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(width / 85);
}
function handleSelect(key, keyPath) {
currentIndex.value = key;
const route = routers.value.find(item => item.path === key);
if (isHttp(key)) {
// http(s)://
window.open(key, "_blank");
} else if (!route || !route.children) {
//
const routeMenu = childrenMenus.value.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
router.push({ path: key, query: query });
} else {
router.push({ path: key });
}
appStore.toggleSideBarHide(true);
} else {
//
activeRoutes(key);
appStore.toggleSideBarHide(false);
}
}
function activeRoutes(key) {
let routes = [];
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
permissionStore.setSidebarRouters(routes);
} else {
appStore.toggleSideBarHide(true);
}
return routes;
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script> </script>
<style lang="scss"> <style lang="scss">
@ -178,13 +179,13 @@ export default {
margin: 0 10px !important; margin: 0 10px !important;
} }
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title { .topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important; border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133; color: #303133;
} }
/* submenu item */ /* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title { .topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left; float: left;
height: 50px !important; height: 50px !important;
line-height: 50px !important; line-height: 50px !important;
@ -192,4 +193,22 @@ export default {
padding: 0 5px !important; padding: 0 5px !important;
margin: 0 10px !important; margin: 0 10px !important;
} }
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
background-color: #ffffff !important;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
}
</style> </style>

@ -0,0 +1,156 @@
<template>
<div class="el-tree-select">
<el-select
style="width: 100%"
v-model="valueId"
ref="treeSelect"
:filterable="true"
:clearable="true"
@clear="clearHandle"
:filter-method="selectFilterData"
:placeholder="placeholder"
>
<el-option :value="valueId" :label="valueTitle">
<el-tree
id="tree-option"
ref="selectTree"
:accordion="accordion"
:data="options"
:props="objMap"
:node-key="objMap.value"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKey"
:filter-node-method="filterNode"
@node-click="handleNodeClick"
></el-tree>
</el-option>
</el-select>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
const props = defineProps({
/* 配置项 */
objMap: {
type: Object,
default: () => {
return {
value: 'id', // ID
label: 'label', //
children: 'children' //
}
}
},
/* 自动收起 */
accordion: {
type: Boolean,
default: () => {
return false
}
},
/**当前双向数据绑定的值 */
value: {
type: [String, Number],
default: ''
},
/**当前的数据 */
options: {
type: Array,
default: () => []
},
/**输入框内部的文字 */
placeholder: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:value']);
const valueId = computed({
get: () => props.value,
set: (val) => {
emit('update:value', val)
}
});
const valueTitle = ref('');
const defaultExpandedKey = ref([]);
function initHandle() {
nextTick(() => {
const selectedValue = valueId.value;
if(selectedValue !== null && typeof (selectedValue) !== 'undefined') {
const node = proxy.$refs.selectTree.getNode(selectedValue)
if (node) {
valueTitle.value = node.data[props.objMap.label]
proxy.$refs.selectTree.setCurrentKey(selectedValue) //
defaultExpandedKey.value = [selectedValue] //
}
} else {
clearHandle()
}
})
}
function handleNodeClick(node) {
valueTitle.value = node[props.objMap.label]
valueId.value = node[props.objMap.value];
defaultExpandedKey.value = [];
proxy.$refs.treeSelect.blur()
selectFilterData('')
}
function selectFilterData(val) {
proxy.$refs.selectTree.filter(val)
}
function filterNode(value, data) {
if (!value) return true
return data[props.objMap['label']].indexOf(value) !== -1
}
function clearHandle() {
valueTitle.value = ''
valueId.value = ''
defaultExpandedKey.value = [];
clearSelected()
}
function clearSelected() {
const allNode = document.querySelectorAll('#tree-option .el-tree-node')
allNode.forEach((element) => element.classList.remove('is-current'))
}
onMounted(() => {
initHandle()
})
watch(valueId, () => {
initHandle();
})
</script>
<style lang='scss' scoped>
@import "@/assets/styles/variables.module.scss";
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
padding: 0;
background-color: #fff;
height: auto;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
ul li .el-tree .el-tree-node__content {
height: auto;
padding: 0 20px;
box-sizing: border-box;
}
:deep(.el-tree-node__content:hover),
:deep(.el-tree-node__content:active),
:deep(.is-current > div:first-child),
:deep(.el-tree-node__content:focus) {
background-color: mix(#fff, $--color-primary, 90%);
color: $--color-primary;
}
</style>

@ -1,36 +1,31 @@
<template> <template>
<div v-loading="loading" :style="'height:' + height"> <div v-loading="loading" :style="'height:' + height">
<iframe <iframe
:src="src" :src="url"
frameborder="no" frameborder="no"
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
scrolling="auto" scrolling="auto" />
/>
</div> </div>
</template> </template>
<script>
export default { <script setup>
props: { const props = defineProps({
src: { src: {
type: String, type: String,
required: true required: true
},
},
data() {
return {
height: document.documentElement.clientHeight - 94.5 + "px;",
loading: true,
url: this.src
};
},
mounted: function () {
setTimeout(() => {
this.loading = false;
}, 300);
const that = this;
window.onresize = function temp() {
that.height = document.documentElement.clientHeight - 94.5 + "px;";
};
} }
}; })
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
const loading = ref(true)
const url = computed(() => props.src)
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 300);
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;";
};
})
</script> </script>

@ -0,0 +1,66 @@
/**
* v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi
*/
export default {
beforeMount(el, { value, arg }) {
if (arg === "callback") {
el.$copyCallback = value;
} else {
el.$copyValue = value;
const handler = () => {
copyTextToClipboard(el.$copyValue);
if (el.$copyCallback) {
el.$copyCallback(el.$copyValue);
}
};
el.addEventListener("click", handler);
el.$destroyCopy = () => el.removeEventListener("click", handler);
}
}
}
function copyTextToClipboard(input, { target = document.body } = {}) {
const element = document.createElement('textarea');
const previouslyFocusedElement = document.activeElement;
element.value = input;
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '');
element.style.contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.fontSize = '12pt'; // Prevent zooming on iOS
const selection = document.getSelection();
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
target.append(element);
element.select();
// Explicit selection workaround for iOS
element.selectionStart = 0;
element.selectionEnd = input.length;
let isSuccess = false;
try {
isSuccess = document.execCommand('copy');
} catch { }
element.remove();
if (originalRange) {
selection.removeAllRanges();
selection.addRange(originalRange);
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
return isSuccess;
}

@ -1,64 +0,0 @@
/**
* v-dialogDrag 弹窗拖拽
* Copyright (c) 2019 ruoyi
*/
export default {
bind(el, binding, vnode, oldVnode) {
const value = binding.value
if (value == false) return
// 获取拖拽内容头部
const dialogHeaderEl = el.querySelector('.el-dialog__header');
const dragDom = el.querySelector('.el-dialog');
dialogHeaderEl.style.cursor = 'move';
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
dragDom.style.position = 'absolute';
dragDom.style.marginTop = 0;
let width = dragDom.style.width;
if (width.includes('%')) {
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
} else {
width = +width.replace(/\px/g, '');
}
dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
// 鼠标按下事件
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
const disX = e.clientX - dialogHeaderEl.offsetLeft;
const disY = e.clientY - dialogHeaderEl.offsetTop;
// 获取到的值带px 正则匹配替换
let styL, styT;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
} else {
styL = +sty.left.replace(/\px/g, '');
styT = +sty.top.replace(/\px/g, '');
};
// 鼠标拖拽事件
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
const l = e.clientX - disX;
const t = e.clientY - disY;
let finallyL = l + styL
let finallyT = t + styT
// 移动当前元素
dragDom.style.left = `${finallyL}px`;
dragDom.style.top = `${finallyT}px`;
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
};
}
}
};

@ -1,34 +0,0 @@
/**
* v-dialogDragWidth 可拖动弹窗高度右下角
* Copyright (c) 2019 ruoyi
*/
export default {
bind(el) {
const dragDom = el.querySelector('.el-dialog');
const lineEl = document.createElement('div');
lineEl.style = 'width: 6px; background: inherit; height: 10px; position: absolute; right: 0; bottom: 0; margin: auto; z-index: 1; cursor: nwse-resize;';
lineEl.addEventListener('mousedown',
function(e) {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - el.offsetLeft;
const disY = e.clientY - el.offsetTop;
// 当前宽度 高度
const curWidth = dragDom.offsetWidth;
const curHeight = dragDom.offsetHeight;
document.onmousemove = function(e) {
e.preventDefault(); // 移动时禁用默认事件
// 通过事件委托,计算移动的距离
const xl = e.clientX - disX;
const yl = e.clientY - disY
dragDom.style.width = `${curWidth + xl}px`;
dragDom.style.height = `${curHeight + yl}px`;
};
document.onmouseup = function(e) {
document.onmousemove = null;
document.onmouseup = null;
};
}, false);
dragDom.appendChild(lineEl);
}
}

@ -1,30 +0,0 @@
/**
* v-dialogDragWidth 可拖动弹窗宽度右侧边
* Copyright (c) 2019 ruoyi
*/
export default {
bind(el) {
const dragDom = el.querySelector('.el-dialog');
const lineEl = document.createElement('div');
lineEl.style = 'width: 5px; background: inherit; height: 80%; position: absolute; right: 0; top: 0; bottom: 0; margin: auto; z-index: 1; cursor: w-resize;';
lineEl.addEventListener('mousedown',
function (e) {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - el.offsetLeft;
// 当前宽度
const curWidth = dragDom.offsetWidth;
document.onmousemove = function (e) {
e.preventDefault(); // 移动时禁用默认事件
// 通过事件委托,计算移动的距离
const l = e.clientX - disX;
dragDom.style.width = `${curWidth + l}px`;
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
};
}, false);
dragDom.appendChild(lineEl);
}
}

@ -1,23 +1,9 @@
import hasRole from './permission/hasRole' import hasRole from './permission/hasRole'
import hasPermi from './permission/hasPermi' import hasPermi from './permission/hasPermi'
import dialogDrag from './dialog/drag' import copyText from './common/copyText'
import dialogDragWidth from './dialog/dragWidth'
import dialogDragHeight from './dialog/dragHeight'
import clipboard from './module/clipboard'
const install = function(Vue) { export default function directive(app){
Vue.directive('hasRole', hasRole) app.directive('hasRole', hasRole)
Vue.directive('hasPermi', hasPermi) app.directive('hasPermi', hasPermi)
Vue.directive('clipboard', clipboard) app.directive('copyText', copyText)
Vue.directive('dialogDrag', dialogDrag)
Vue.directive('dialogDragWidth', dialogDragWidth)
Vue.directive('dialogDragHeight', dialogDragHeight)
} }
if (window.Vue) {
window['hasRole'] = hasRole
window['hasPermi'] = hasPermi
Vue.use(install); // eslint-disable-line
}
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;
}
}
}

@ -3,13 +3,13 @@
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
import store from '@/store' import useUserStore from '@/store/modules/user'
export default { export default {
inserted(el, binding, vnode) { mounted(el, binding, vnode) {
const { value } = binding const { value } = binding
const all_permission = "*:*:*"; const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions const permissions = useUserStore().permissions
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value const permissionFlag = value

@ -3,13 +3,13 @@
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
import store from '@/store' import useUserStore from '@/store/modules/user'
export default { export default {
inserted(el, binding, vnode) { mounted(el, binding, vnode) {
const { value } = binding const { value } = binding
const super_admin = "admin"; const super_admin = "admin";
const roles = store.getters && store.getters.roles const roles = useUserStore().roles
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const roleFlag = value const roleFlag = value
@ -22,7 +22,7 @@ export default {
el.parentNode && el.parentNode.removeChild(el) el.parentNode && el.parentNode.removeChild(el)
} }
} else { } else {
throw new Error(`请设置角色权限标签值"`) throw new Error(`请设置角色权限标签值`)
} }
} }
} }

@ -1,29 +1,21 @@
<template> <template>
<section class="app-main"> <section class="app-main">
<transition name="fade-transform" mode="out-in"> <router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews"> <transition name="fade-transform" mode="out-in">
<router-view v-if="!$route.meta.link" :key="key" /> <keep-alive :include="tagsViewStore.cachedViews">
</keep-alive> <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
</transition> </keep-alive>
</transition>
</router-view>
<iframe-toggle /> <iframe-toggle />
</section> </section>
</template> </template>
<script> <script setup>
import iframeToggle from "./IframeToggle/index" import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView'
export default { const tagsViewStore = useTagsViewStore()
name: 'AppMain',
components: { iframeToggle },
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -73,3 +65,4 @@ export default {
border-radius: 3px; border-radius: 3px;
} }
</style> </style>

@ -1,33 +1,25 @@
<template> <template>
<transition-group name="fade-transform" mode="out-in"> <inner-link
<inner-link v-for="(item, index) in tagsViewStore.iframeViews"
v-for="(item, index) in iframeViews" :key="item.path"
:key="item.path" :iframeId="'iframe' + index"
:iframeId="'iframe' + index" v-show="route.path === item.path"
v-show="$route.path === item.path" :src="iframeUrl(item.meta.link, item.query)"
:src="iframeUrl(item.meta.link, item.query)" ></inner-link>
></inner-link>
</transition-group>
</template> </template>
<script> <script setup>
import InnerLink from "../InnerLink/index"; import InnerLink from "../InnerLink/index";
import useTagsViewStore from "@/store/modules/tagsView";
export default { const route = useRoute();
components: { InnerLink }, const tagsViewStore = useTagsViewStore();
computed: {
iframeViews() { function iframeUrl(url, query) {
return this.$store.state.tagsView.iframeViews; if (Object.keys(query).length > 0) {
} let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
}, return url + "?" + params;
methods: {
iframeUrl(url, query) {
if (Object.keys(query).length > 0) {
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
return url + "?" + params;
}
return url;
}
} }
return url;
} }
</script> </script>

@ -1,5 +1,5 @@
<template> <template>
<div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!"> <div :style="'height:' + height">
<iframe <iframe
:id="iframeId" :id="iframeId"
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
@ -9,39 +9,16 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
props: { src: {
src: { type: String,
type: String, default: "/"
default: "/"
},
iframeId: {
type: String
}
}, },
data() { iframeId: {
return { type: String
loading: false,
height: document.documentElement.clientHeight - 94.5 + "px;"
};
},
mounted() {
var _this = this;
const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/");
const iframe = document.querySelector(iframeId);
// iframeloading
if (iframe.attachEvent) {
this.loading = true;
iframe.attachEvent("onload", function () {
_this.loading = false;
});
} else {
this.loading = true;
iframe.onload = function () {
_this.loading = false;
};
}
} }
}; });
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
</script> </script>

@ -1,13 +1,12 @@
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/> <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu"> <div class="right-menu">
<template v-if="device!=='mobile'"> <template v-if="appStore.device !== 'mobile'">
<search id="header-search" class="right-menu-item" /> <header-search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom"> <el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
@ -22,112 +21,103 @@
<el-tooltip content="布局大小" effect="dark" placement="bottom"> <el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" /> <size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
</template> </template>
<div class="avatar-container">
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="avatar" class="user-avatar"> <img :src="userStore.avatar" class="user-avatar" />
<i class="el-icon-caret-bottom" /> <el-icon><caret-bottom /></el-icon>
</div> </div>
<el-dropdown-menu slot="dropdown"> <template #dropdown>
<router-link to="/user/profile"> <el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item> <router-link to="/user/profile">
</router-link> <el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click.native="setting = true"> </router-link>
<span>布局设置</span> <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
</el-dropdown-item> <span>布局设置</span>
<el-dropdown-item divided @click.native="logout"> </el-dropdown-item>
<span>退出登录</span> <el-dropdown-item divided command="logout">
</el-dropdown-item> <span>退出登录</span>
</el-dropdown-menu> </el-dropdown-item>
</el-dropdown> </el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import { mapGetters } from 'vuex' import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav' import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull' import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect' import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch' import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git' import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc' import RuoYiDoc from '@/components/RuoYi/Doc'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
export default { const appStore = useAppStore()
components: { const userStore = useUserStore()
Breadcrumb, const settingsStore = useSettingsStore()
TopNav,
Hamburger, function toggleSideBar() {
Screenfull, appStore.toggleSideBar()
SizeSelect, }
Search,
RuoYiGit, function handleCommand(command) {
RuoYiDoc switch (command) {
}, case "setLayout":
computed: { setLayout();
...mapGetters([ break;
'sidebar', case "logout":
'avatar', logout();
'device' break;
]), default:
setting: { break;
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {});
}
} }
} }
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logOut().then(() => {
location.href = '/index';
})
}).catch(() => { });
}
const emits = defineEmits(['setLayout'])
function setLayout() {
emits('setLayout');
}
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.navbar { .navbar {
height: 50px; height: 50px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: #fff; background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container { .hamburger-container {
line-height: 46px; line-height: 46px;
height: 100%; height: 100%;
float: left; float: left;
cursor: pointer; cursor: pointer;
transition: background .3s; transition: background 0.3s;
-webkit-tap-highlight-color:transparent; -webkit-tap-highlight-color: transparent;
&:hover { &:hover {
background: rgba(0, 0, 0, .025) background: rgba(0, 0, 0, 0.025);
} }
} }
@ -149,6 +139,7 @@ export default {
float: right; float: right;
height: 100%; height: 100%;
line-height: 50px; line-height: 50px;
display: flex;
&:focus { &:focus {
outline: none; outline: none;
@ -164,16 +155,16 @@ export default {
&.hover-effect { &.hover-effect {
cursor: pointer; cursor: pointer;
transition: background .3s; transition: background 0.3s;
&:hover { &:hover {
background: rgba(0, 0, 0, .025) background: rgba(0, 0, 0, 0.025);
} }
} }
} }
.avatar-container { .avatar-container {
margin-right: 30px; margin-right: 40px;
.avatar-wrapper { .avatar-wrapper {
margin-top: 5px; margin-top: 5px;
@ -186,7 +177,7 @@ export default {
border-radius: 10px; border-radius: 10px;
} }
.el-icon-caret-bottom { i {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: -20px; right: -20px;

@ -1,260 +1,205 @@
<template> <template>
<el-drawer size="280px" :visible="visible" :with-header="false" :append-to-body="true" :show-close="false"> <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<div class="drawer-container"> <div class="setting-drawer-title">
<div> <h3 class="drawer-title">主题风格设置</h3>
<div class="setting-drawer-content"> </div>
<div class="setting-drawer-title"> <div class="setting-drawer-block-checbox">
<h3 class="drawer-title">主题风格设置</h3> <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
</div> <img src="@/assets/images/dark.svg" alt="dark" />
<div class="setting-drawer-block-checbox"> <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')"> <i aria-label=": check" class="anticon anticon-check">
<img src="@/assets/images/dark.svg" alt="dark"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;"> <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" />
<i aria-label=": check" class="anticon anticon-check"> </svg>
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class=""> </i>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light">
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label=": check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
</div> </div>
</div>
<el-divider/> <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light" />
<h3 class="drawer-title">系统布局配置</h3> <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label=": check" class="anticon anticon-check">
<div class="drawer-item"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<span>开启 TopNav</span> <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" />
<el-switch v-model="topNav" class="drawer-switch" /> </svg>
</i>
</div> </div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<span class="comp-style">
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
</span>
</div>
<el-divider />
<div class="drawer-item"> <h3 class="drawer-title">系统布局配置</h3>
<span>开启 Tags-Views</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>固定 Header</span> <span>开启 TopNav</span>
<el-switch v-model="fixedHeader" class="drawer-switch" /> <span class="comp-style">
</div> <el-switch v-model="settingsStore.topNav" @change="topNavChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>显示 Logo</span> <span>开启 Tags-Views</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" /> <span class="comp-style">
</div> <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>动态标题</span> <span>固定 Header</span>
<el-switch v-model="dynamicTitle" class="drawer-switch" /> <span class="comp-style">
</div> <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</span>
</div>
<el-divider/> <div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</span>
</div>
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting"></el-button> <div class="drawer-item">
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting"></el-button> <span>动态标题</span>
</div> <span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
</span>
</div> </div>
<el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting"></el-button>
<el-button plain icon="Refresh" @click="resetSetting"></el-button>
</el-drawer> </el-drawer>
</template>
<script> </template>
import ThemePicker from '@/components/ThemePicker'
export default { <script setup>
components: { ThemePicker }, import variables from '@/assets/styles/variables.module.scss'
data() { import axios from 'axios'
return { import { ElLoading, ElMessage } from 'element-plus'
theme: this.$store.state.settings.theme, import { useDynamicTitle } from '@/utils/dynamicTitle'
sideTheme: this.$store.state.settings.sideTheme import useAppStore from '@/store/modules/app'
}; import useSettingsStore from '@/store/modules/settings'
}, import usePermissionStore from '@/store/modules/permission'
computed: { import { handleThemeStyle } from '@/utils/theme'
visible: {
get() { const { proxy } = getCurrentInstance();
return this.$store.state.settings.showSettings const appStore = useAppStore()
} const settingsStore = useSettingsStore()
}, const permissionStore = usePermissionStore()
fixedHeader: { const showSettings = ref(false);
get() { const theme = ref(settingsStore.theme);
return this.$store.state.settings.fixedHeader const sideTheme = ref(settingsStore.sideTheme);
}, const storeSettings = computed(() => settingsStore);
set(val) { const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader', /** 是否需要topnav */
value: val function topNavChange(val) {
}) if (!val) {
} appStore.toggleSideBarHide(false);
}, permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
topNav: {
get() {
return this.$store.state.settings.topNav
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'topNav',
value: val
})
if (!val) {
this.$store.dispatch('app/toggleSideBarHide', false);
this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes);
}
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
},
dynamicTitle: {
get() {
return this.$store.state.settings.dynamicTitle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'dynamicTitle',
value: val
})
}
},
},
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;
},
saveSetting() {
this.$modal.loading("正在保存到本地,请稍候...");
this.$cache.local.set(
"layout-setting",
`{
"topNav":${this.topNav},
"tagsView":${this.tagsView},
"fixedHeader":${this.fixedHeader},
"sidebarLogo":${this.sidebarLogo},
"dynamicTitle":${this.dynamicTitle},
"sideTheme":"${this.sideTheme}",
"theme":"${this.theme}"
}`
);
setTimeout(this.$modal.closeLoading(), 1000)
},
resetSetting() {
this.$modal.loading("正在清除设置缓存并刷新,请稍候...");
this.$cache.local.remove("layout-setting")
setTimeout("window.location.reload()", 1000)
}
} }
} }
</script>
<style lang="scss" scoped>
.setting-drawer-content {
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
font-weight: bold;
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img { function themeChange(val) {
width: 48px; settingsStore.theme = val;
height: 48px; handleThemeStyle(val);
} }
function handleTheme(val) {
settingsStore.sideTheme = val;
sideTheme.value = val;
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000)
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000)
}
function openSetting() {
showSettings.value = true;
}
.setting-drawer-block-checbox-selectIcon { defineExpose({
position: absolute; openSetting,
top: 0; })
right: 0; </script>
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
}
.drawer-container { <style lang='scss' scoped>
padding: 20px; .setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px; font-size: 14px;
line-height: 1.5; }
word-wrap: break-word; }
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.drawer-title { .custom-img {
margin-bottom: 12px; width: 48px;
color: rgba(0, 0, 0, .85); height: 38px;
font-size: 14px; border-radius: 5px;
line-height: 22px; box-shadow: 1px 1px 2px #898484;
} }
.drawer-item { .setting-drawer-block-checbox-selectIcon {
color: rgba(0, 0, 0, .65); position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px; font-size: 14px;
padding: 12px 0;
} }
}
}
.drawer-switch { .drawer-item {
float: right color: rgba(0, 0, 0, 0.65);
} padding: 12px 0;
font-size: 14px;
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
} }
}
</style> </style>

@ -1,25 +0,0 @@
export default {
computed: {
device() {
return this.$store.state.app.device
}
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
}
}
}

@ -1,33 +0,0 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
vnodes.push(<svg-icon icon-class={icon}/>)
}
if (title) {
if (title.length > 5) {
vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
} else {
vnodes.push(<span slot='title'>{(title)}</span>)
}
}
return vnodes
}
}
</script>

@ -1,43 +1,40 @@
<template> <template>
<component :is="type" v-bind="linkProps(to)"> <component :is="type" v-bind="linkProps()">
<slot /> <slot />
</component> </component>
</template> </template>
<script> <script setup>
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
export default { const props = defineProps({
props: { to: {
to: { type: [String, Object],
type: [String, Object], required: true
required: true }
} })
},
computed: { const isExt = computed(() => {
isExternal() { return isExternal(props.to)
return isExternal(this.to) })
},
type() { const type = computed(() => {
if (this.isExternal) { if (isExt.value) {
return 'a' return 'a'
} }
return 'router-link' return 'router-link'
} })
},
methods: { function linkProps() {
linkProps(to) { if (isExt.value) {
if (this.isExternal) { return {
return { href: props.to,
href: to, target: '_blank',
target: '_blank', rel: 'noopener'
rel: 'noopener'
}
}
return {
to: to
}
} }
} }
return {
to: props.to
}
} }
</script> </script>

@ -1,45 +1,33 @@
<template> <template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> <div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" /> <img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1> <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" /> <img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1> <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link> </router-link>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script setup>
import logoImg from '@/assets/logo/logo.png' import variables from '@/assets/styles/variables.module.scss'
import variables from '@/assets/styles/variables.scss' import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
export default { defineProps({
name: 'SidebarLogo', collapse: {
props: { type: Boolean,
collapse: { required: true
type: Boolean,
required: true
}
},
computed: {
variables() {
return variables;
},
sideTheme() {
return this.$store.state.settings.sideTheme
}
},
data() {
return {
title: process.env.VUE_APP_TITLE,
logo: logoImg
}
} }
} })
const title = import.meta.env.VITE_APP_TITLE;
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

@ -1,17 +1,20 @@
<template> <template>
<div v-if="!item.hidden"> <div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
<template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
</el-menu-item> </el-menu-item>
</app-link> </app-link>
</template> </template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template slot="title"> <template v-if="item.meta" #title>
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> <svg-icon :icon-class="item.meta && item.meta.icon" />
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
</template> </template>
<sidebar-item <sidebar-item
v-for="(child, index) in item.children" v-for="(child, index) in item.children"
:key="child.path + index" :key="child.path + index"
@ -20,81 +23,80 @@
:base-path="resolvePath(child.path)" :base-path="resolvePath(child.path)"
class="nest-menu" class="nest-menu"
/> />
</el-submenu> </el-sub-menu>
</div> </div>
</template> </template>
<script> <script setup>
import path from 'path'
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link' import AppLink from './Link'
import FixiOSBug from './FixiOSBug' import { getNormalPath } from '@/utils/ruoyi'
export default { const props = defineProps({
name: 'SidebarItem', // route object
components: { Item, AppLink }, item: {
mixins: [FixiOSBug], type: Object,
props: { required: true
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
}, },
data() { isNest: {
this.onlyOneChild = null type: Boolean,
return {} default: false
}, },
methods: { basePath: {
hasOneShowingChild(children = [], parent) { type: String,
if (!children) { default: ''
children = []; }
} })
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display const onlyOneChild = ref({});
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false return false
}, } else {
resolvePath(routePath, routeQuery) { // Temp set(will be used if only has one showing child)
if (isExternal(routePath)) { onlyOneChild.value = item
return routePath return true
}
if (isExternal(this.basePath)) {
return this.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: path.resolve(this.basePath, routePath), query: query }
}
return path.resolve(this.basePath, routePath)
} }
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
};
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
}
return getNormalPath(props.basePath + '/' + routePath)
}
function hasTitle(title){
if (title.length > 5) {
return title;
} else {
return "";
} }
} }
</script> </script>

@ -1,57 +1,54 @@
<template> <template>
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<logo v-if="showLogo" :collapse="isCollapse" /> <logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper"> <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
:collapse="isCollapse" :collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true" :unique-opened="true"
:active-text-color="settings.theme" :active-text-color="theme"
:collapse-transition="false" :collapse-transition="false"
mode="vertical" mode="vertical"
> >
<sidebar-item <sidebar-item
v-for="(route, index) in sidebarRouters" v-for="(route, index) in sidebarRouters"
:key="route.path + index" :key="route.path + index"
:item="route" :item="route"
:base-path="route.path" :base-path="route.path"
/> />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
<script> <script setup>
import { mapGetters, mapState } from "vuex"; import Logo from './Logo'
import Logo from "./Logo"; import SidebarItem from './SidebarItem'
import SidebarItem from "./SidebarItem"; import variables from '@/assets/styles/variables.module.scss'
import variables from "@/assets/styles/variables.scss"; import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
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 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;
})
export default {
components: { SidebarItem, Logo },
computed: {
...mapState(["settings"]),
...mapGetters(["sidebarRouters", "sidebar"]),
activeMenu() {
const route = this.$route;
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> </script>

@ -1,94 +1,105 @@
<template> <template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot /> <slot />
</el-scrollbar> </el-scrollbar>
</template> </template>
<script> <script setup>
const tagAndTagSpacing = 4 // tagAndTagSpacing import useTagsViewStore from '@/store/modules/tagsView'
export default { const tagAndTagSpacing = ref(4);
name: 'ScrollPane', const { proxy } = getCurrentInstance();
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeDestroy() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
let firstTag = null const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
let lastTag = null
// find first tag and last tag onMounted(() => {
if (tagList.length > 0) { scrollWrapper.value.addEventListener('scroll', emitScroll, true)
firstTag = tagList[0] })
lastTag = tagList[tagList.length - 1] onBeforeUnmount(() => {
} scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = scrollWrapper.value;
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits()
const emitScroll = () => {
emits('scroll')
}
if (firstTag === currentTag) { const tagsViewStore = useTagsViewStore()
$scrollWrapper.scrollLeft = 0 const visitedViews = computed(() => tagsViewStore.visitedViews);
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag function moveToTarget(currentTag) {
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollWrapper.value;
// the tag's offsetLeft before of prevTag let firstTag = null
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing let lastTag = null
// find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0]
lastTag = visitedViews.value[visitedViews.value.length - 1]
}
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth $scrollWrapper.scrollLeft = 0
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { } else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const tagListDom = document.getElementsByClassName('tags-view-item');
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
let prevTag = null
let nextTag = null
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
prevTag = tagListDom[k];
}
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
nextTag = tagListDom[k];
} }
} }
} }
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
} }
} }
defineExpose({
moveToTarget,
})
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.scroll-container { .scroll-container {
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
::v-deep { :deep(.el-scrollbar__bar) {
.el-scrollbar__bar { bottom: 0px;
bottom: 0px; }
} :deep(.el-scrollbar__wrap) {
.el-scrollbar__wrap { height: 39px;
height: 49px;
}
} }
} }
</style> </style>

@ -1,249 +1,253 @@
<template> <template>
<div id="tags-view-container" class="tags-view-container"> <div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll"> <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
<router-link <router-link
v-for="tag in visitedViews" v-for="tag in visitedViews"
ref="tag"
:key="tag.path" :key="tag.path"
:class="isActive(tag)?'active':''" :data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item" class="tags-view-item"
:style="activeStyle(tag)" :style="activeStyle(tag)"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag,$event)" @contextmenu.prevent="openMenu(tag, $event)"
> >
{{ tag.title }} {{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
</span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li> <li @click="refreshSelectedTag(selectedTag)">
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> </li> <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li> </li>
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> </li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> </li> <close style="width: 1em; height: 1em;" /> 关闭当前
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li> </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> </ul>
</div> </div>
</template> </template>
<script> <script setup>
import ScrollPane from './ScrollPane' import ScrollPane from './ScrollPane'
import path from 'path' import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from '@/store/modules/tagsView'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
export default { const visible = ref(false);
components: { ScrollPane }, const top = ref(0);
data() { const left = ref(0);
return { const selectedTag = ref({});
visible: false, const affixTags = ref([]);
top: 0, const scrollPaneRef = ref(null);
left: 0,
selectedTag: {}, const { proxy } = getCurrentInstance();
affixTags: [] const route = useRoute();
} const router = useRouter();
},
computed: { const visitedViews = computed(() => useTagsViewStore().visitedViews);
visitedViews() { const routes = computed(() => usePermissionStore().routes);
return this.$store.state.tagsView.visitedViews const theme = computed(() => useSettingsStore().theme);
},
routes() { watch(route, () => {
return this.$store.state.permission.routes addTags()
}, moveToCurrentTag()
theme() { })
return this.$store.state.settings.theme; watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
})
onMounted(() => {
initTags()
addTags()
})
function isActive(r) {
return r.path === route.path
}
function activeStyle(tag) {
if (!isActive(tag)) return {};
return {
"background-color": theme.value,
"border-color": theme.value
};
}
function isAffix(tag) {
return tag.meta && tag.meta.affix
}
function isFirstView() {
try {
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
} catch (err) {
return false
}
}
function isLastView() {
try {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
} catch (err) {
return false
}
}
function filterAffixTags(routes, basePath = '') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = getNormalPath(basePath + '/' + route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
} }
}, if (route.children) {
watch: { const tempTags = filterAffixTags(route.children, route.path)
$route() { if (tempTags.length >= 1) {
this.addTags() tags = [...tags, ...tempTags]
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
} }
} }
}, })
mounted() { return tags
this.initTags() }
this.addTags() function initTags() {
}, const res = filterAffixTags(routes.value);
methods: { affixTags.value = res;
isActive(route) { for (const tag of res) {
return route.path === this.$route.path // Must have tag name
}, if (tag.name) {
activeStyle(tag) { useTagsViewStore().addVisitedView(tag)
if (!this.isActive(tag)) return {}; }
return { }
"background-color": this.theme, }
"border-color": this.theme function addTags() {
}; const { name } = route
}, if (name) {
isAffix(tag) { useTagsViewStore().addView(route)
return tag.meta && tag.meta.affix if (route.meta.link) {
}, useTagsViewStore().addIframeView(route);
isFirstView() { }
try { }
return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath return false
} catch (err) { }
return false function moveToCurrentTag() {
} nextTick(() => {
}, for (const r of visitedViews.value) {
isLastView() { if (r.path === route.path) {
try { scrollPaneRef.value.moveToTarget(r);
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath // when query is different then update
} catch (err) { if (r.fullPath !== route.fullPath) {
return false useTagsViewStore().updateVisitedView(route)
}
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/addIframeView', this.$route)
}
}
return false
},
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)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view);
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/delIframeView', this.$route)
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(()=>{});
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
} }
} }
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
} }
})
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view);
if (route.meta.link) {
useTagsViewStore().delIframeView(route);
}
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => { });
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag()
})
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
return
}
toLastView(visitedViews, view)
})
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath })
} else {
router.push('/')
}
}
}
function openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
const offsetWidth = proxy.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const l = e.clientX - offsetLeft + 15 // 15: margin right
if (l > maxLeft) {
left.value = maxLeft
} else {
left.value = l
} }
top.value = e.clientY
visible.value = true
selectedTag.value = tag
}
function closeMenu() {
visible.value = false
}
function handleScroll() {
closeMenu()
} }
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.tags-view-container { .tags-view-container {
height: 34px; height: 34px;
width: 100%; width: 100%;
background: #fff; background: #fff;
border-bottom: 1px solid #d8dce5; border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper { .tags-view-wrapper {
.tags-view-item { .tags-view-item {
display: inline-block; display: inline-block;
@ -269,14 +273,14 @@ export default {
color: #fff; color: #fff;
border-color: #42b983; border-color: #42b983;
&::before { &::before {
content: ''; content: "";
background: #fff; background: #fff;
display: inline-block; display: inline-block;
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
position: relative; position: relative;
margin-right: 2px; margin-right: 5px;
} }
} }
} }
@ -292,7 +296,7 @@ export default {
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
color: #333; color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li { li {
margin: 0; margin: 0;
padding: 7px 16px; padding: 7px 16px;
@ -315,16 +319,18 @@ export default {
vertical-align: 2px; vertical-align: 2px;
border-radius: 50%; border-radius: 50%;
text-align: center; text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1); transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%; transform-origin: 100% 50%;
&:before { &:before {
transform: scale(.6); transform: scale(0.6);
display: inline-block; display: inline-block;
vertical-align: -3px; vertical-align: -3px;
} }
&:hover { &:hover {
background-color: #b4bccc; background-color: #b4bccc;
color: #fff; color: #fff;
width: 12px !important;
height: 12px !important;
} }
} }
} }

@ -1,5 +1,4 @@
export { default as AppMain } from './AppMain' export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar' export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings' export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue' export { default as TagsView } from './TagsView/index.vue'

@ -1,111 +1,111 @@
<template> <template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}"> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/> <sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container"> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> <div :class="{ 'fixed-header': fixedHeader }">
<navbar/> <navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView"/> <tags-view v-if="needTagsView" />
</div> </div>
<app-main/> <app-main />
<right-panel> <settings ref="settingRef" />
<settings/>
</right-panel>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import RightPanel from '@/components/RightPanel' import { useWindowSize } from '@vueuse/core'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components' import Sidebar from './components/Sidebar/index.vue'
import ResizeMixin from './mixin/ResizeHandler' import { AppMain, Navbar, Settings, TagsView } from './components'
import { mapState } from 'vuex' import defaultSettings from '@/settings'
import variables from '@/assets/styles/variables.scss'
import useAppStore from '@/store/modules/app'
export default { import useSettingsStore from '@/store/modules/settings'
name: 'Layout',
components: { const settingsStore = useSettingsStore()
AppMain, const theme = computed(() => settingsStore.theme);
Navbar, const sideTheme = computed(() => settingsStore.sideTheme);
RightPanel, const sidebar = computed(() => useAppStore().sidebar);
Settings, const device = computed(() => useAppStore().device);
Sidebar, const needTagsView = computed(() => settingsStore.tagsView);
TagsView const fixedHeader = computed(() => settingsStore.fixedHeader);
},
mixins: [ResizeMixin], const classObj = computed(() => ({
computed: { hideSidebar: !sidebar.value.opened,
...mapState({ openSidebar: sidebar.value.opened,
theme: state => state.settings.theme, withoutAnimation: sidebar.value.withoutAnimation,
sideTheme: state => state.settings.sideTheme, mobile: device.value === 'mobile'
sidebar: state => state.app.sidebar, }))
device: state => state.app.device,
needTagsView: state => state.settings.tagsView, const { width, height } = useWindowSize();
fixedHeader: state => state.settings.fixedHeader const WIDTH = 992; // refer to Bootstrap's responsive design
}),
classObj() { watchEffect(() => {
return { if (device.value === 'mobile' && sidebar.value.opened) {
hideSidebar: !this.sidebar.opened, useAppStore().closeSideBar({ withoutAnimation: false })
openSidebar: this.sidebar.opened, }
withoutAnimation: this.sidebar.withoutAnimation, if (width.value - 1 < WIDTH) {
mobile: this.device === 'mobile' useAppStore().toggleDevice('mobile')
} useAppStore().closeSideBar({ withoutAnimation: true })
}, } else {
variables() { useAppStore().toggleDevice('desktop')
return variables;
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
} }
})
function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false })
}
const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss"; @import "@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss"; @import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg { .app-wrapper {
background: #000; @include clearfix;
opacity: 0.3; position: relative;
width: 100%; height: 100%;
top: 0; width: 100%;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header { &.mobile.openSidebar {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
} }
}
.hideSidebar .fixed-header { .drawer-bg {
width: calc(100% - 54px); background: #000;
} opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.sidebarHide .fixed-header { .fixed-header {
width: 100%; position: fixed;
} top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.mobile .fixed-header { .hideSidebar .fixed-header {
width: 100%; width: calc(100% - 54px);
} }
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style> </style>

@ -1,45 +0,0 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}

@ -1,28 +1,36 @@
import Vue from 'vue' import { createApp } from 'vue'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import Element from 'element-ui' import ElementPlus from 'element-plus'
import './assets/styles/element-variables.scss' import 'element-plus/dist/index.css'
import locale from 'element-plus/es/locale/lang/zh-cn'
import '@/assets/styles/index.scss' // global css import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App' import App from './App'
import store from './store' import store from './store'
import router from './router' import router from './router'
import directive from './directive' // directive import directive from './directive' // directive
// 注册指令
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
import { download } from '@/utils/request' import { download } from '@/utils/request'
import './assets/icons' // icon // svg图标
import 'virtual:svg-icons-register'
import SvgIcon from '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config"; import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"; import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 分页组件 // 分页组件
import Pagination from "@/components/Pagination"; import Pagination from '@/components/Pagination'
// 自定义表格工具组件 // 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar" import RightToolbar from '@/components/RightToolbar'
// 富文本组件 // 富文本组件
import Editor from "@/components/Editor" import Editor from "@/components/Editor"
// 文件上传组件 // 文件上传组件
@ -31,56 +39,46 @@ import FileUpload from "@/components/FileUpload"
import ImageUpload from "@/components/ImageUpload" import ImageUpload from "@/components/ImageUpload"
// 图片预览组件 // 图片预览组件
import ImagePreview from "@/components/ImagePreview" import ImagePreview from "@/components/ImagePreview"
// 自定义树选择组件
import TreeSelect from '@/components/TreeSelect'
// 字典标签组件 // 字典标签组件
import DictTag from '@/components/DictTag' import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta' const app = createApp(App)
// 字典数据组件
import DictData from '@/components/DictData'
// 全局方法挂载 // 全局方法挂载
Vue.prototype.getDicts = getDicts app.config.globalProperties.useDict = useDict
Vue.prototype.getConfigKey = getConfigKey app.config.globalProperties.download = download
Vue.prototype.parseTime = parseTime app.config.globalProperties.parseTime = parseTime
Vue.prototype.resetForm = resetForm app.config.globalProperties.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange app.config.globalProperties.handleTree = handleTree
Vue.prototype.selectDictLabel = selectDictLabel app.config.globalProperties.addDateRange = addDateRange
Vue.prototype.selectDictLabels = selectDictLabels app.config.globalProperties.selectDictLabel = selectDictLabel
Vue.prototype.download = download app.config.globalProperties.selectDictLabels = selectDictLabels
Vue.prototype.handleTree = handleTree
// 全局组件挂载 // 全局组件挂载
Vue.component('DictTag', DictTag) app.component('DictTag', DictTag)
Vue.component('Pagination', Pagination) app.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar) app.component('TreeSelect', TreeSelect)
Vue.component('Editor', Editor) app.component('FileUpload', FileUpload)
Vue.component('FileUpload', FileUpload) app.component('ImageUpload', ImageUpload)
Vue.component('ImageUpload', ImageUpload) app.component('ImagePreview', ImagePreview)
Vue.component('ImagePreview', ImagePreview) app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
Vue.config.productionTip = false app.use(router)
app.use(store)
app.use(plugins)
app.use(elementIcons)
app.component('svg-icon', SvgIcon)
new Vue({ directive(app)
el: '#app',
router, // 使用element-plus 并且设置全局的大小
store, app.use(ElementPlus, {
render: h => h(App) locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
}) })
app.mount('#app')

@ -1,19 +1,22 @@
import router from './router' import router from './router'
import store from './store' import { ElMessage } from 'element-plus'
import { Message } from 'element-ui'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import { isRelogin } from '@/utils/request' import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'] const whiteList = ['/login', '/register'];
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start()
if (getToken()) { if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title) to.meta.title && useSettingsStore().setTitle(to.meta.title)
/* has token*/ /* has token*/
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })
@ -21,22 +24,26 @@ router.beforeEach((to, from, next) => {
} else if (whiteList.indexOf(to.path) !== -1) { } else if (whiteList.indexOf(to.path) !== -1) {
next() next()
} else { } else {
if (store.getters.roles.length === 0) { if (useUserStore().roles.length === 0) {
isRelogin.show = true isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息 // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => { useUserStore().getInfo().then(() => {
isRelogin.show = false isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => { usePermissionStore().generateRoutes().then(accessRoutes => {
// 根据roles权限生成可访问的路由表 // 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表 accessRoutes.forEach(route => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
}) })
}).catch(err => { }).catch(err => {
store.dispatch('LogOut').then(() => { useUserStore().logOut().then(() => {
Message.error(err) ElMessage.error(err)
next({ path: '/' }) next({ path: '/' })
})
}) })
})
} else { } else {
next() next()
} }
@ -47,7 +54,7 @@ router.beforeEach((to, from, next) => {
// 在免登录白名单,直接进入 // 在免登录白名单,直接进入
next() next()
} else { } else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done() NProgress.done()
} }
} }

@ -1,8 +1,8 @@
import store from '@/store' import useUserStore from '@/store/modules/user'
function authPermission(permission) { function authPermission(permission) {
const all_permission = "*:*:*"; const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions const permissions = useUserStore().permissions
if (permission && permission.length > 0) { if (permission && permission.length > 0) {
return permissions.some(v => { return permissions.some(v => {
return all_permission === v || v === permission return all_permission === v || v === permission
@ -14,7 +14,7 @@ function authPermission(permission) {
function authRole(role) { function authRole(role) {
const super_admin = "admin"; const super_admin = "admin";
const roles = store.getters && store.getters.roles const roles = useUserStore().roles
if (role && role.length > 0) { if (role && role.length > 0) {
return roles.some(v => { return roles.some(v => {
return super_admin === v || v === role return super_admin === v || v === role

@ -1,17 +1,17 @@
import axios from 'axios' import axios from 'axios'
import {Loading, Message} from 'element-ui' import { ElLoading, ElMessage } from 'element-plus'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode' import errorCode from '@/utils/errorCode'
import { blobValidate } from "@/utils/ruoyi"; import { blobValidate } from '@/utils/ruoyi'
const baseURL = process.env.VUE_APP_BASE_API const baseURL = import.meta.env.VITE_APP_BASE_API
let downloadLoadingInstance; let downloadLoadingInstance;
export default { export default {
zip(url, name) { zip(url, name) {
var url = baseURL + url var url = baseURL + url
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }) downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
axios({ axios({
method: 'get', method: 'get',
url: url, url: url,
@ -28,7 +28,7 @@ export default {
downloadLoadingInstance.close(); downloadLoadingInstance.close();
}).catch((r) => { }).catch((r) => {
console.error(r) console.error(r)
Message.error('下载文件出现错误,请联系管理员!') ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close(); downloadLoadingInstance.close();
}) })
}, },
@ -39,7 +39,7 @@ export default {
const resText = await data.text(); const resText = await data.text();
const rspObj = JSON.parse(resText); const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg); ElMessage.error(errMsg);
} }
} }

@ -4,17 +4,15 @@ import cache from './cache'
import modal from './modal' import modal from './modal'
import download from './download' import download from './download'
export default { export default function installPlugins(app){
install(Vue) { // 页签操作
// 页签操作 app.config.globalProperties.$tab = tab
Vue.prototype.$tab = tab // 认证对象
// 认证对象 app.config.globalProperties.$auth = auth
Vue.prototype.$auth = auth // 缓存对象
// 缓存对象 app.config.globalProperties.$cache = cache
Vue.prototype.$cache = cache // 模态框对象
// 模态框对象 app.config.globalProperties.$modal = modal
Vue.prototype.$modal = modal // 下载文件
// 下载文件 app.config.globalProperties.$download = download
Vue.prototype.$download = download
}
} }

@ -1,59 +1,59 @@
import { Message, MessageBox, Notification, Loading } from 'element-ui' import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance; let loadingInstance;
export default { export default {
// 消息提示 // 消息提示
msg(content) { msg(content) {
Message.info(content) ElMessage.info(content)
}, },
// 错误消息 // 错误消息
msgError(content) { msgError(content) {
Message.error(content) ElMessage.error(content)
}, },
// 成功消息 // 成功消息
msgSuccess(content) { msgSuccess(content) {
Message.success(content) ElMessage.success(content)
}, },
// 警告消息 // 警告消息
msgWarning(content) { msgWarning(content) {
Message.warning(content) ElMessage.warning(content)
}, },
// 弹出提示 // 弹出提示
alert(content) { alert(content) {
MessageBox.alert(content, "系统提示") ElMessageBox.alert(content, "系统提示")
}, },
// 错误提示 // 错误提示
alertError(content) { alertError(content) {
MessageBox.alert(content, "系统提示", { type: 'error' }) ElMessageBox.alert(content, "系统提示", { type: 'error' })
}, },
// 成功提示 // 成功提示
alertSuccess(content) { alertSuccess(content) {
MessageBox.alert(content, "系统提示", { type: 'success' }) ElMessageBox.alert(content, "系统提示", { type: 'success' })
}, },
// 警告提示 // 警告提示
alertWarning(content) { alertWarning(content) {
MessageBox.alert(content, "系统提示", { type: 'warning' }) ElMessageBox.alert(content, "系统提示", { type: 'warning' })
}, },
// 通知提示 // 通知提示
notify(content) { notify(content) {
Notification.info(content) ElNotification.info(content)
}, },
// 错误通知 // 错误通知
notifyError(content) { notifyError(content) {
Notification.error(content); ElNotification.error(content);
}, },
// 成功通知 // 成功通知
notifySuccess(content) { notifySuccess(content) {
Notification.success(content) ElNotification.success(content)
}, },
// 警告通知 // 警告通知
notifyWarning(content) { notifyWarning(content) {
Notification.warning(content) ElNotification.warning(content)
}, },
// 确认窗体 // 确认窗体
confirm(content) { confirm(content) {
return MessageBox.confirm(content, "系统提示", { return ElMessageBox.confirm(content, "系统提示", {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: "warning", type: "warning",
@ -61,7 +61,7 @@ export default {
}, },
// 提交内容 // 提交内容
prompt(content) { prompt(content) {
return MessageBox.prompt(content, "系统提示", { return ElMessageBox.prompt(content, "系统提示", {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: "warning", type: "warning",
@ -69,10 +69,9 @@ export default {
}, },
// 打开遮罩层 // 打开遮罩层
loading(content) { loading(content) {
loadingInstance = Loading.service({ loadingInstance = ElLoading.service({
lock: true, lock: true,
text: content, text: content,
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)", background: "rgba(0, 0, 0, 0.7)",
}) })
}, },

@ -1,10 +1,10 @@
import store from '@/store' import useTagsViewStore from '@/store/modules/tagsView'
import router from '@/router'; import router from '@/router'
export default { export default {
// 刷新当前tab页签 // 刷新当前tab页签
refreshPage(obj) { refreshPage(obj) {
const { path, query, matched } = router.currentRoute; const { path, query, matched } = router.currentRoute.value;
if (obj === undefined) { if (obj === undefined) {
matched.forEach((m) => { matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) { if (m.components && m.components.default && m.components.default.name) {
@ -14,7 +14,7 @@ export default {
} }
}); });
} }
return store.dispatch('tagsView/delCachedView', obj).then(() => { return useTagsViewStore().delCachedView(obj).then(() => {
const { path, query } = obj const { path, query } = obj
router.replace({ router.replace({
path: '/redirect' + path, path: '/redirect' + path,
@ -24,7 +24,7 @@ export default {
}, },
// 关闭当前tab页签打开新页签 // 关闭当前tab页签打开新页签
closeOpenPage(obj) { closeOpenPage(obj) {
store.dispatch("tagsView/delView", router.currentRoute); useTagsViewStore().delView(router.currentRoute.value);
if (obj !== undefined) { if (obj !== undefined) {
return router.push(obj); return router.push(obj);
} }
@ -32,7 +32,7 @@ export default {
// 关闭指定tab页签 // 关闭指定tab页签
closePage(obj) { closePage(obj) {
if (obj === undefined) { if (obj === undefined) {
return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => { return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
const latestView = visitedViews.slice(-1)[0] const latestView = visitedViews.slice(-1)[0]
if (latestView) { if (latestView) {
return router.push(latestView.fullPath) return router.push(latestView.fullPath)
@ -40,32 +40,30 @@ export default {
return router.push('/'); return router.push('/');
}); });
} }
return store.dispatch('tagsView/delView', obj); return useTagsViewStore().delView(obj);
}, },
// 关闭所有tab页签 // 关闭所有tab页签
closeAllPage() { closeAllPage() {
return store.dispatch('tagsView/delAllViews'); return useTagsViewStore().delAllViews();
}, },
// 关闭左侧tab页签 // 关闭左侧tab页签
closeLeftPage(obj) { closeLeftPage(obj) {
return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute); return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
}, },
// 关闭右侧tab页签 // 关闭右侧tab页签
closeRightPage(obj) { closeRightPage(obj) {
return store.dispatch('tagsView/delRightTags', obj || router.currentRoute); return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
}, },
// 关闭其他tab页签 // 关闭其他tab页签
closeOtherPage(obj) { closeOtherPage(obj) {
return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute); return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
}, },
// 添加tab页签 // 打开tab页签
openPage(title, url, params) { openPage(url) {
const obj = { path: url, meta: { title: title } } return router.push(url);
store.dispatch('tagsView/addView', obj);
return router.push({ path: url, query: params });
}, },
// 修改tab页签 // 修改tab页签
updatePage(obj) { updatePage(obj) {
return store.dispatch('tagsView/updateVisitedView', obj); return useTagsViewStore().updateVisitedView(obj);
} }
} }

@ -1,8 +1,4 @@
import Vue from 'vue' import { createWebHistory, createRouter } from 'vue-router'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */ /* Layout */
import Layout from '@/layout' import Layout from '@/layout'
@ -37,7 +33,7 @@ export const constantRoutes = [
children: [ children: [
{ {
path: '/redirect/:path(.*)', path: '/redirect/:path(.*)',
component: () => import('@/views/redirect') component: () => import('@/views/redirect/index.vue')
} }
] ]
}, },
@ -52,7 +48,7 @@ export const constantRoutes = [
hidden: true hidden: true
}, },
{ {
path: '/404', path: "/:pathMatch(.*)*",
component: () => import('@/views/error/404'), component: () => import('@/views/error/404'),
hidden: true hidden: true
}, },
@ -64,10 +60,10 @@ export const constantRoutes = [
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: 'index', redirect: '/index',
children: [ children: [
{ {
path: 'index', path: '/index',
component: () => import('@/views/index'), component: () => import('@/views/index'),
name: 'Index', name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: { title: '首页', icon: 'dashboard', affix: true }
@ -164,20 +160,16 @@ export const dynamicRoutes = [
} }
] ]
// 防止连续点击多次路由报错 const router = createRouter({
let routerPush = Router.prototype.push; history: createWebHistory(),
let routerReplace = Router.prototype.replace; routes: constantRoutes,
// push scrollBehavior(to, from, savedPosition) {
Router.prototype.push = function push(location) { if (savedPosition) {
return routerPush.call(this, location).catch(err => err) return savedPosition
} } else {
// replace return { top: 0 }
Router.prototype.replace = function push(location) { }
return routerReplace.call(this, location).catch(err => err) },
} });
export default new Router({ export default router;
mode: 'history', // 去掉url中的#
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})

@ -1,13 +1,16 @@
module.exports = { export default {
/**
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/** /**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light * 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/ */
sideTheme: 'theme-dark', sideTheme: 'theme-dark',
/** /**
* 是否系统布局配置 * 是否系统布局配置
*/ */
showSettings: false, showSettings: true,
/** /**
* 是否显示顶部导航 * 是否显示顶部导航

@ -1,19 +0,0 @@
const getters = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
dict: state => state.dict.dict,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permissions: state => state.user.permissions,
permission_routes: state => state.permission.routes,
topbarRouters:state => state.permission.topbarRouters,
defaultRoutes:state => state.permission.defaultRoutes,
sidebarRouters:state => state.permission.sidebarRouters,
}
export default getters

@ -1,25 +1,3 @@
import Vue from 'vue' const store = createPinia()
import Vuex from 'vuex'
import app from './modules/app'
import dict from './modules/dict'
import user from './modules/user'
import tagsView from './modules/tagsView'
import permission from './modules/permission'
import settings from './modules/settings'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
dict,
user,
tagsView,
permission,
settings
},
getters
})
export default store export default store

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

Loading…
Cancel
Save