This commit is contained in:
季万俊 2025-10-13 09:55:15 +08:00
parent 5818aaa21a
commit da0b86992e
21 changed files with 1994 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

BIN
src/assets/logo/logo-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
src/assets/logo/logo-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/logo/logo-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src/assets/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,99 @@
@use './variables.module.scss' as *;
@mixin colorBtn($color) {
background: $color;
&:hover {
color: $color;
&:before,
&:after {
background: $color;
}
}
}
.blue-btn {
@include colorBtn($blue)
}
.light-blue-btn {
@include colorBtn($light-blue)
}
.red-btn {
@include colorBtn($red)
}
.pink-btn {
@include colorBtn($pink)
}
.green-btn {
@include colorBtn($green)
}
.tiffany-btn {
@include colorBtn($tiffany)
}
.yellow-btn {
@include colorBtn($yellow)
}
.pan-btn {
font-size: 14px;
color: #fff;
padding: 14px 36px;
border-radius: 8px;
border: none;
outline: none;
transition: 600ms ease all;
position: relative;
display: inline-block;
&:hover {
background: #fff;
&:before,
&:after {
width: 100%;
transition: 600ms ease all;
}
}
&:before,
&:after {
content: '';
position: absolute;
top: 0;
right: 0;
height: 2px;
width: 0;
transition: 400ms ease all;
}
&::after {
right: inherit;
top: inherit;
left: 0;
bottom: 0;
}
}
.custom-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
color: #fff;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: 0;
margin: 0;
padding: 10px 15px;
font-size: 14px;
border-radius: 4px;
}

View File

@ -0,0 +1,92 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
.cell {
.el-tag {
margin-right: 0px;
}
}
.small-padding {
.cell {
padding-left: 5px;
padding-right: 5px;
}
}
.fixed-width {
.el-button--mini {
padding: 7px 10px;
width: 60px;
}
}
.status-col {
.cell {
padding: 0 10px;
text-align: center;
.el-tag {
margin-right: 0px;
}
}
}
// to fixed https://github.com/ElemeFE/element/issues/2461
// .el-dialog {
// transform: none;
// left: 0;
// position: relative;
// margin: 0 auto;
// }
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
display: inline-flex !important;
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
.el-menu--collapse
> div
> .el-submenu
> .el-submenu__title
.el-submenu__icon-arrow {
display: none;
}

View File

@ -0,0 +1,31 @@
/**
* 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;
}

View File

@ -0,0 +1,195 @@
@use './variables.module.scss' as *;
@use './mixin.scss' as *;
@use './transition.scss' as *;
@use './element-ui.scss' as *;
@use './sidebar.scss' as *;
@use './btn.scss' as *;
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
.no-padding {
padding: 0px !important;
}
.padding-content {
padding: 4px 0;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.fr {
float: right;
}
.fl {
float: left;
}
.pr-5 {
padding-right: 5px;
}
.pl-5 {
padding-left: 5px;
}
.block {
display: block;
}
.pointer {
cursor: pointer;
}
.inlineBlock {
display: block;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
aside {
background: #eef1f6;
padding: 8px 24px;
margin-bottom: 20px;
border-radius: 2px;
display: block;
line-height: 32px;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
color: #2c3e50;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
a {
color: #337ab7;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
}
}
}
.el-select,.el-time-picker,.el-range-editor {
width: 100%;
}
.el-time-picker {
width: 100%;
}
.el-range-editor--medium.el-input__inner {
width: 100%;
}
.el-date-editor {
width: 100% !important;
}
//main-container全局样式
.app-container {
padding: 20px;
height: calc(100vh - 84px);
overflow-y: scroll;
}
.components-container {
margin: 30px 50px;
position: relative;
}
.pagination-container {
margin-top: 30px;
}
.text-center {
text-align: center
}
.sub-navbar {
height: 50px;
line-height: 50px;
position: relative;
width: 100%;
text-align: right;
padding-right: 20px;
transition: 600ms ease position;
background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
.subtitle {
font-size: 20px;
color: #fff;
}
&.draft {
background: #d0d0d0;
}
&.deleted {
background: #d0d0d0;
}
}
.link-type,
.link-type:focus {
color: #337ab7;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
}
}
.filter-container {
padding-bottom: 10px;
.filter-item {
display: inline-block;
vertical-align: middle;
margin-bottom: 10px;
}
}

View File

@ -0,0 +1,66 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin pct($pct) {
width: #{$pct};
position: relative;
margin: 0 auto;
}
@mixin triangle($width, $height, $color, $direction) {
$width: $width/2;
$color-border-style: $height solid $color;
$transparent-border-style: $width solid transparent;
height: 0;
width: 0;
@if $direction==up {
border-bottom: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
}
@else if $direction==right {
border-left: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
@else if $direction==down {
border-top: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
}
@else if $direction==left {
border-right: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
}

View File

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

View File

@ -0,0 +1,232 @@
@use './variables.module.scss' as *;
#app {
.main-container {
height: 100%;
transition: margin-left .28s;
margin-left: $base-sidebar-width;
position: relative;
}
.sidebarHide {
margin-left: 0!important;
}
.sidebar-container {
-webkit-transition: width .28s;
transition: width 0.28s;
width: $base-sidebar-width !important;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
box-shadow: 2px 0 6px rgba(0,21,41,.35);
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
// display: inline-block;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
.el-menu-item {
width: 100%;
}
.el-menu-item, .el-submenu__title {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
& .theme-dark .is-active > .el-submenu__title {
color: $base-menu-color-active !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $base-sidebar-width !important;
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
& .theme-dark .nest-menu .el-submenu>.el-submenu__title,
& .theme-dark .el-submenu .el-menu-item {
background-color: $base-sub-menu-background !important;
&:hover {
background-color: $base-sub-menu-hover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $base-sidebar-width !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $base-sidebar-width !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$base-sidebar-width, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}

View File

@ -0,0 +1,49 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform--move,
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

View File

@ -0,0 +1,221 @@
// base color
$blue: #324157;
$light-blue: #333c46;
$red: #C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow: #FEC171;
$panGreen: #30B08F;
// 默认主题变量
$menuText: #bfcbd9;
$menuActiveText: #409eff;
$menuBg: #304156;
$menuHover: #263445;
// 浅色主题theme-light
$menuLightBg: #ffffff;
$menuLightHover: #f0f1f5;
$menuLightText: #303133;
$menuLightActiveText: #409EFF;
// 基础变量
$base-sidebar-width: 200px;
$sideBarWidth: 200px;
// 菜单暗色变量
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 组件变量
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
menuLightBg: $menuLightBg;
menuLightHover: $menuLightHover;
menuLightText: $menuLightText;
menuLightActiveText: $menuLightActiveText;
sideBarWidth: $sideBarWidth;
// 导出基础颜色
blue: $blue;
lightBlue: $light-blue;
red: $red;
pink: $pink;
green: $green;
tiffany: $tiffany;
yellow: $yellow;
panGreen: $panGreen;
// 导出组件颜色
colorPrimary: $--color-primary;
colorSuccess: $--color-success;
colorWarning: $--color-warning;
colorDanger: $--color-danger;
colorInfo: $--color-info;
}
// CSS变量定义
:root {
/* 亮色模式变量 */
--sidebar-bg: #{$menuBg};
--sidebar-text: #{$menuText};
--menu-hover: #{$menuHover};
--navbar-bg: #ffffff;
--navbar-text: #303133;
/* splitpanes default-theme 变量 */
--splitpanes-default-bg: #ffffff;
}
// 暗黑模式变量
html.dark {
/* 默认通用 */
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #ffffff;
--el-text-color-regular: #d0d0d0;
--el-border-color: #434343;
--el-border-color-light: #434343;
/* 侧边栏 */
--sidebar-bg: #141414;
--sidebar-text: #ffffff;
--menu-hover: #2d2d2d;
--menu-active-text: #{$menuActiveText};
/* 顶部导航栏 */
--navbar-bg: #141414;
--navbar-text: #ffffff;
--navbar-hover: #141414;
/* 标签栏 */
--tags-bg: #141414;
--tags-item-bg: #1d1e1f;
--tags-item-border: #303030;
--tags-item-text: #d0d0d0;
--tags-item-hover: #2d2d2d;
--tags-close-hover: #64666a;
/* splitpanes 组件暗黑模式变量 */
--splitpanes-bg: #141414;
--splitpanes-border: #303030;
--splitpanes-splitter-bg: #1d1e1f;
--splitpanes-splitter-hover-bg: #2d2d2d;
/* blockquote 暗黑模式变量 */
--blockquote-bg: #1d1e1f;
--blockquote-border: #303030;
--blockquote-text: #d0d0d0;
/* Cron 时间表达式 模式变量 */
--cron-border: #303030;
/* splitpanes default-theme 暗黑模式变量 */
--splitpanes-default-bg: #141414;
/* 侧边栏菜单覆盖 */
.sidebar-container {
.el-menu-item, .menu-title {
color: var(--el-text-color-regular);
}
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: var(--el-bg-color) !important;
}
}
/* 顶部栏栏菜单覆盖 */
.el-menu--horizontal {
.el-menu-item {
&:not(.is-disabled) {
&:hover,
&:focus {
background-color: var(--navbar-hover) !important;
}
}
}
}
/* 分割窗格覆盖 */
.splitpanes {
background-color: var(--splitpanes-bg);
.splitpanes__pane {
background-color: var(--splitpanes-bg);
border-color: var(--splitpanes-border);
}
.splitpanes__splitter {
background-color: var(--splitpanes-splitter-bg);
border-color: var(--splitpanes-border);
&:hover {
background-color: var(--splitpanes-splitter-hover-bg);
}
&:before,
&:after {
background-color: var(--splitpanes-border);
}
}
}
/* 表格样式覆盖 */
.el-table {
--el-table-header-bg-color: var(--el-bg-color-overlay) !important;
--el-table-header-text-color: var(--el-text-color-regular) !important;
--el-table-border-color: var(--el-border-color-light) !important;
--el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
th {
background-color: var(--el-bg-color-overlay, #f8f8f9) !important;
color: var(--el-text-color-regular, #515a6e);
}
}
}
/* 树组件高亮样式覆盖 */
.el-tree {
.el-tree-node.is-current > .el-tree-node__content {
background-color: var(--el-bg-color-overlay) !important;
color: var(--el-color-primary);
}
.el-tree-node__content:hover {
background-color: var(--el-bg-color-overlay);
}
}
/* 下拉菜单样式覆盖 */
.el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{
background-color: var(--navbar-hover) !important;
}
/* blockquote样式覆盖 */
blockquote {
background-color: var(--blockquote-bg) !important;
border-left-color: var(--blockquote-border) !important;
color: var(--blockquote-text) !important;
}
/* 时间表达式标题样式覆盖 */
.popup-result .title {
background: var(--cron-border);
}
}

View File

@ -0,0 +1,98 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import { usePermissionStore } from '@/store/modules/permission'
const route = useRoute()
const router = useRouter()
const permissionStore = usePermissionStore()
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = []
const pathNum = findPathNum(route.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = route.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
getMatched(pathList, permissionStore.defaultRoutes, matched)
} else {
matched = route.matched.filter((item) => item.meta && item.meta.title)
}
//
if (!isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
num++
index = str.indexOf(char, index + 1)
}
return num
}
function getMatched(pathList, routeList, matched) {
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
if (data) {
matched.push(data)
if (data.children && pathList.length) {
pathList.shift()
getMatched(pathList, data.children, matched)
}
}
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
router.push(redirect)
return
}
router.push(path)
}
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
})
getBreadcrumb()
</script>
<style lang='scss' scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,313 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane label="秒" v-if="shouldHide('second')">
<CrontabSecond
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronsecond"
/>
</el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmin"
/>
</el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronhour"
/>
</el-tab-pane>
<el-tab-pane label="日" v-if="shouldHide('day')">
<CrontabDay
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronday"
/>
</el-tab-pane>
<el-tab-pane label="月" v-if="shouldHide('month')">
<CrontabMonth
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmonth"
/>
</el-tab-pane>
<el-tab-pane label="周" v-if="shouldHide('week')">
<CrontabWeek
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronweek"
/>
</el-tab-pane>
<el-tab-pane label="年" v-if="shouldHide('year')">
<CrontabYear
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronyear"
/>
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<table>
<thead>
<tr>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
</td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tr>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button type="primary" @click="submitFill">确定</el-button>
<el-button type="warning" @click="clearCron">重置</el-button>
<el-button @click="hidePopup">取消</el-button>
</div>
</div>
</div>
</template>
<script setup>
import CrontabSecond from "./second.vue"
import CrontabMin from "./min.vue"
import CrontabHour from "./hour.vue"
import CrontabDay from "./day.vue"
import CrontabMonth from "./month.vue"
import CrontabWeek from "./week.vue"
import CrontabYear from "./year.vue"
import CrontabResult from "./result.vue"
const { proxy } = getCurrentInstance()
const emit = defineEmits(['hide', 'fill'])
const props = defineProps({
hideComponent: {
type: Array,
default: () => [],
},
expression: {
type: String,
default: ""
}
})
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
const tabActive = ref(0)
const hideComponent = ref([])
const expression = ref('')
const crontabValueObj = ref({
second: "*",
min: "*",
hour: "*",
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) {
//6
let obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
month: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : ""
}
crontabValueObj.value = {
...obj,
}
}
} else {
//
clearCron()
}
}
// tab
function tabCheck(index) {
tabActive.value = index
}
//
function updateCrontabValue(name, value, from) {
crontabValueObj.value[name] = value
}
// -props
function checkNumber(value, minLimit, maxLimit) {
//
value = Math.floor(value)
if (value < minLimit) {
value = minLimit
} else if (value > maxLimit) {
value = maxLimit
}
return value
}
//
function hidePopup() {
emit("hide")
}
//
function submitFill() {
emit("fill", crontabValueString.value)
hidePopup()
}
function clearCron() {
//
crontabValueObj.value = {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
}
onMounted(() => {
expression.value = props.expression
hideComponent.value = props.hideComponent
})
</script>
<style lang="scss" scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
box-sizing: border-box;
line-height: 24px;
margin: 25px auto;
padding: 15px 10px 10px;
border: 1px solid #ccc;
position: relative;
}
.popup-result .title {
position: absolute;
top: -28px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table td:not(.result) {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
</style>