refactor(frontend): 重构前端样式体系,统一资源路径与代码规范

1.  重构SCSS样式结构,将原有分散的样式文件整合到custom目录下,统一使用@use替代@import
2.  删除冗余的全局样式文件,将样式逻辑内联到对应组件或集中到core目录
3.  新增DOMPurify依赖,对富文本内容添加XSS防护
4.  修复metrics端点暴露的兼容性问题,优化启动banner样式
5.  移除无用的权限校验代码,重构工作流组件路径与命名
6.  优化样式混合宏与响应式布局,修复部分组件样式冲突
This commit is contained in:
zhangtao
2026-06-01 00:58:21 +08:00
parent c2135bb456
commit c0733d9861
50 changed files with 1880 additions and 1792 deletions
@@ -258,5 +258,277 @@ onBeforeUnmount(() => {
</script>
<style lang="scss">
@use "@styles/fa-wang-editor";
$box-radius: calc(var(--custom-radius) / 3 + 2px);
/* 全屏容器 z-index 调整 */
.w-e-full-screen-container {
z-index: 100 !important;
}
/* 编辑器容器 */
.editor-wrapper {
width: 100%;
height: 100%;
border: 1px solid var(--fa-gray-300);
border-radius: $box-radius !important;
.w-e-bar {
border-radius: $box-radius $box-radius 0 0 !important;
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
i {
margin-right: 5px;
}
}
/* 工具栏 */
.editor-toolbar {
border-bottom: 1px solid var(--default-border);
}
/* 下拉选择框配置 */
.w-e-select-list {
min-width: 140px;
padding: 5px 10px 10px;
border: none;
border-radius: $box-radius;
}
/* 下拉选择框元素配置 */
.w-e-select-list ul li {
margin-top: 5px;
font-size: 15px !important;
border-radius: $box-radius;
}
/* 下拉选择框 正文文字大小调整 */
.w-e-select-list ul li:last-of-type {
font-size: 16px !important;
}
/* 下拉选择框 hover 样式调整 */
.w-e-select-list ul li:hover {
background-color: var(--fa-gray-200);
}
:root {
/* 激活颜色 */
--w-e-toolbar-active-bg-color: var(--fa-gray-200);
/* toolbar 图标和文字颜色 */
--w-e-toolbar-color: #000;
/* 表格选中时候的边框颜色 */
--w-e-textarea-selected-border-color: #ddd;
/* 表格头背景颜色 */
--w-e-textarea-slight-bg-color: var(--fa-gray-200);
}
/* 工具栏按钮样式 */
.w-e-bar-item svg {
fill: var(--fa-gray-800);
}
.w-e-bar-item button {
color: var(--fa-gray-800);
border-radius: $box-radius;
}
/* 工具栏 hover 按钮背景颜色 */
.w-e-bar-item button:hover {
background-color: var(--fa-gray-200);
}
/* 工具栏分割线 */
.w-e-bar-divider {
height: 20px;
margin-top: 10px;
background-color: #ccc;
}
/* 工具栏菜单 */
.w-e-bar-item-group .w-e-bar-item-menus-container {
min-width: 120px;
padding: 10px 0;
border: none;
border-radius: $box-radius;
.w-e-bar-item {
button {
width: 100%;
margin: 0 5px;
}
}
}
/* 代码块 */
.w-e-text-container [data-slate-editor] pre > code {
padding: 0.6rem 1rem;
background-color: var(--fa-gray-50);
border-radius: $box-radius;
}
/* 弹出框 */
.w-e-drop-panel {
border: 0;
border-radius: $box-radius;
}
a {
color: #318ef4;
}
.w-e-text-container {
[data-slate-editor] {
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0.8em 0 0.4em;
font-weight: 700;
line-height: 1.35;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1.125em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: 0.875em;
}
ul,
ol {
padding-left: 1.5em;
margin: 0.8em 0;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li {
margin: 0.25em 0;
}
ul ul {
list-style: circle;
}
ul ul ul {
list-style: square;
}
}
strong,
b {
font-weight: 700;
}
i,
em {
font-style: italic;
}
}
/* 表格样式优化 */
.w-e-text-container [data-slate-editor] .table-container th {
border-right: none;
}
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
border-right: 1px solid #ccc !important;
}
/* 引用 */
.w-e-text-container [data-slate-editor] blockquote {
background-color: var(--fa-gray-200);
border-left: 4px solid var(--fa-gray-300);
}
/* 输入区域弹出 bar */
.w-e-hover-bar {
border-radius: $box-radius;
}
/* 超链接弹窗 */
.w-e-modal {
border: none;
border-radius: $box-radius;
}
/* 图片样式调整 */
.w-e-text-container [data-slate-editor] .w-e-selected-image-container {
overflow: inherit;
&:hover {
border: 0;
}
img {
border: 1px solid transparent;
transition: border 0.3s;
&:hover {
border: 1px solid #318ef4 !important;
}
}
.w-e-image-dragger {
width: 12px;
height: 12px;
background-color: #318ef4;
border: 2px solid #fff;
border-radius: $box-radius;
}
.left-top {
top: -6px;
left: -6px;
}
.right-top {
top: -6px;
right: -6px;
}
.left-bottom {
bottom: -6px;
left: -6px;
}
.right-bottom {
right: -6px;
bottom: -6px;
}
}
}
</style>
@@ -362,19 +362,526 @@ watch(menuOpen, (isMenuOpen: boolean) => {
</script>
<style lang="scss" scoped>
@use "@styles/fa-sidebar-menu";
.layout-sidebar {
display: flex;
height: 100vh;
user-select: none;
scrollbar-width: none;
border-right: 1px solid var(--fa-card-border);
&.no-border {
border-right: none !important;
}
:deep(.el-scrollbar__bar.is-vertical) {
width: 4px;
}
:deep(.el-scrollbar__thumb) {
right: -2px;
background-color: #ccc;
border-radius: 2px;
}
.dual-menu-left {
position: relative;
width: 80px;
height: 100%;
border-right: 1px solid var(--fa-card-border) !important;
transition: width 0.25s;
.logo {
margin: auto;
margin-top: 12px;
margin-bottom: 3px;
cursor: pointer;
}
ul {
li {
> div {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 8px;
overflow: hidden;
text-align: center;
cursor: pointer;
border-radius: 5px;
.art-svg-icon {
display: block;
margin: 0 auto;
font-size: 20px;
}
span {
display: -webkit-box;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
line-clamp: 1;
font-size: 12px;
-webkit-box-orient: vertical;
}
&.is-active {
background: var(--el-color-primary-light-9);
.art-svg-icon,
span {
color: var(--theme-color) !important;
}
}
}
}
}
.switch-btn {
position: absolute;
right: 0;
bottom: 15px;
left: 0;
margin: auto;
}
}
.menu-left {
position: relative;
box-sizing: border-box;
height: 100vh;
@media only screen and (width <= 640px) {
height: 100dvh;
}
.el-menu {
height: 100%;
}
&:hover {
.dual-menu-collapse-btn {
opacity: 1 !important;
}
}
.dual-menu-collapse-btn {
position: absolute;
top: 50%;
right: -11px;
z-index: 10;
width: 11px;
height: 50px;
cursor: pointer;
background-color: var(--default-box-color);
border: 1px solid var(--fa-card-border);
border-radius: 0 15px 15px 0;
opacity: 0;
transform: translateY(-50%);
transition: opacity 0.2s;
&:hover {
.art-svg-icon {
color: var(--fa-gray-800) !important;
}
}
.art-svg-icon {
position: absolute;
top: 0;
bottom: 0;
left: -4px;
margin: auto;
transition: all 0.3s;
}
}
}
.header {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
height: 60px;
overflow: hidden;
line-height: 60px;
cursor: pointer;
.logo {
margin-left: 22px;
}
p {
position: absolute;
top: 0;
bottom: 0;
left: 58px;
box-sizing: border-box;
margin-left: 10px;
font-size: 18px;
&.is-dual-menu-name {
left: 25px;
margin: auto;
}
}
}
.el-menu {
box-sizing: border-box;
/* 防止菜单内的滚动影响整个页面滚动 */
overscroll-behavior: contain;
scrollbar-width: none;
border-right: 0;
-ms-scroll-chaining: contain;
&::-webkit-scrollbar {
width: 0 !important;
}
}
.menu-model {
display: none;
}
}
@media only screen and (width <= 800px) {
.layout-sidebar {
width: 0;
.header {
height: 50px;
line-height: 50px;
}
.el-menu {
height: calc(100vh - 60px);
}
.el-menu--collapse {
width: 0;
}
/* 折叠状态下的header样式 */
.menu-left-close .header {
.logo {
display: none;
}
p {
left: 16px;
font-size: 0;
opacity: 0 !important;
}
}
.menu-model {
position: fixed;
top: 0;
left: 0;
z-index: -1;
display: block;
width: 100%;
height: 100vh;
background: rgba($color: #000, $alpha: 50%);
transition: opacity 0.2s ease-in-out;
}
}
}
@media only screen and (width <= 640px) {
.layout-sidebar {
border-right: 0 !important;
}
}
.dark {
.layout-sidebar {
border-right: 1px solid rgb(255 255 255 / 13%);
:deep(.el-scrollbar__thumb) {
background-color: #777;
}
.dual-menu-left {
border-right: 1px solid rgb(255 255 255 / 9%) !important;
}
}
}
</style>
<style lang="scss">
@use "@styles/fa-sidebar-menu-theme";
@use "@styles/core/mixin.scss" as *;
/* 菜单样式变量 */
$menu-height: 42px;
$menu-icon-size: 20px;
$menu-font-size: 14px;
$hover-bg-color: var(--fa-gray-200);
$popup-menu-height: 40px;
$popup-menu-padding: 8px;
$popup-menu-margin: 5px;
$popup-menu-radius: 6px;
/* 通用菜单项样式 */
@mixin menu-item-base {
width: calc(100% - 16px);
margin-left: 8px;
border-radius: 6px;
.menu-icon {
margin-left: -7px;
}
}
/* 通用 hover 样式 */
@mixin menu-hover($bg-color) {
.el-sub-menu__title:hover,
.el-menu-item:not(.is-active):hover {
background: $bg-color !important;
}
}
/* 通用选中样式 */
@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) {
.el-menu-item.is-active {
color: $color !important;
background-color: $bg-color;
.menu-icon {
.art-svg-icon {
color: $icon-color !important;
}
}
}
}
/* 弹窗菜单项样式 */
@mixin popup-menu-item {
height: $popup-menu-height;
margin-bottom: $popup-menu-margin;
border-radius: $popup-menu-radius;
.menu-icon {
margin-right: 5px;
}
&:last-of-type {
margin-bottom: 0;
}
}
/* 主题菜单通用样式(合并 design 和 dark 主题的共同逻辑) */
@mixin theme-menu-base {
.el-sub-menu__title,
.el-menu-item {
@include menu-item-base;
}
}
/* 弹窗菜单通用样式 */
@mixin popup-menu-base($hover-bg, $active-color, $active-bg) {
.el-menu--popup {
padding: $popup-menu-padding;
.el-sub-menu__title:hover,
.el-menu-item:hover {
background-color: $hover-bg !important;
border-radius: $popup-menu-radius;
}
.el-menu-item {
@include popup-menu-item;
&.is-active {
color: $active-color !important;
background-color: $active-bg !important;
}
}
.el-sub-menu {
@include popup-menu-item;
height: $popup-menu-height !important;
.el-sub-menu__title {
height: $popup-menu-height !important;
border-radius: $popup-menu-radius;
}
}
}
}
.layout-sidebar {
// 展开的宽度
/* ---------------------- Modify default style ---------------------- */
/* 菜单折叠样式 */
.menu-left-close {
.header {
.logo {
margin: 0 auto;
}
}
}
/* 菜单图标 */
.menu-icon {
margin-right: 8px;
font-size: $menu-icon-size;
}
/* 菜单高度 */
.el-sub-menu__title,
.el-menu-item {
height: $menu-height !important;
margin-bottom: 4px;
line-height: $menu-height !important;
span {
font-size: $menu-font-size !important;
@include ellipsis();
}
}
/* 右侧箭头 */
.el-sub-menu__icon-arrow {
width: 13px !important;
font-size: 13px !important;
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu.is-active {
.el-sub-menu__title {
.menu-icon {
.art-svg-icon {
// 选中菜单图标颜色
color: var(--theme-color) !important;
}
}
}
}
}
/* ---------------------- Design theme menu ---------------------- */
.el-menu-design {
@include theme-menu-base;
@include menu-active(var(--theme-color), var(--el-color-primary-light-9));
@include menu-hover($hover-bg-color);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-600);
}
}
/* ---------------------- Dark theme menu ---------------------- */
.el-menu-dark {
@include theme-menu-base;
@include menu-active(#fff, #27282d, #fff);
@include menu-hover(#0f1015);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-400);
}
}
/* ---------------------- Light theme menu ---------------------- */
.el-menu-light {
.el-sub-menu__title,
.el-menu-item {
.menu-icon {
margin-left: 1px;
}
}
.el-menu-item.is-active {
background-color: var(--el-color-primary-light-9);
.art-svg-icon {
color: var(--theme-color) !important;
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
content: "";
background: var(--theme-color);
}
}
@include menu-hover($hover-bg-color);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-600);
}
}
}
@media only screen and (width <= 640px) {
.layout-sidebar {
.el-menu-design {
> .el-sub-menu {
margin-left: 0;
}
.el-sub-menu {
width: 100% !important;
}
}
}
}
/* 菜单折叠 hover 弹窗样式(浅色主题) */
.el-menu--vertical,
.el-menu--popup-container {
@include popup-menu-base(var(--fa-gray-200), var(--fa-gray-900), var(--fa-gray-200));
}
/* 暗黑模式菜单样式 */
.dark {
.el-menu--vertical,
.el-menu--popup-container {
@include popup-menu-base(var(--fa-gray-200), var(--fa-gray-900), #292a2e);
}
.layout-sidebar {
/* 图标颜色、文字颜色 */
.menu-icon .art-svg-icon,
.menu-name {
color: var(--fa-gray-800) !important;
}
/* 选中的文字颜色跟图标颜色 */
.el-menu-item.is-active {
span,
.menu-icon .art-svg-icon {
color: var(--theme-color) !important;
}
}
/* 右侧箭头颜色 */
.el-sub-menu__icon-arrow {
color: #fff;
}
}
}
.layout-sidebar {
/* 展开的宽度 */
.el-menu:not(.el-menu--collapse) {
width: v-bind(menuopenwidth);
}
// 折叠后宽度
/* 折叠后宽度 */
.el-menu--collapse {
width: v-bind(menuclosewidth);
}
@@ -57,5 +57,96 @@ onUnmounted(() => {
</script>
<style lang="scss">
@use "@styles/fa-settings-panel";
@use "@styles/core/mixin.scss" as *;
/* 设置抽屉模态框样式 */
.setting-modal {
background: transparent !important;
.el-drawer {
// 背景滤镜效果
background: rgba($color: #fff, $alpha: 50%) !important;
box-shadow: 0 0 30px rgb(0 0 0 / 10%) !important;
@include backdropBlur();
.setting-box-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
width: calc(100% + 15px);
margin-bottom: 10px;
.setting-item {
box-sizing: border-box;
width: calc(33.333% - 15px);
margin-right: 15px;
text-align: center;
.box {
position: relative;
box-sizing: border-box;
display: flex;
height: 52px;
overflow: hidden;
cursor: pointer;
border: 2px solid var(--default-border);
border-radius: 8px;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%);
transition: box-shadow 0.1s;
&.mt-16 {
margin-top: 16px;
}
&.is-active {
border: 2px solid var(--theme-color);
}
img {
width: 100%;
height: 100%;
}
}
.name {
margin-top: 6px;
font-size: 14px;
text-align: center;
}
}
}
}
// 去除滚动条
.el-drawer__body::-webkit-scrollbar {
width: 0 !important;
}
}
.dark {
.setting-modal {
.el-drawer {
background: rgba($color: #000, $alpha: 50%) !important;
.setting-item {
.box {
border: 2px solid transparent;
}
}
}
}
}
/* 去除火狐浏览器滚动条 */
:deep(.el-drawer__body) {
scrollbar-width: none;
}
/* // 移动端隐藏 */
@media screen and (width <= 800px) {
.mobile-hide {
display: none !important;
}
}
</style>
@@ -64,5 +64,5 @@ function onGuideFinished(): void {
</script>
<style lang="scss" scoped>
@use "@styles/layouts";
@use "@styles/custom/fa-layouts";
</style>
@@ -352,8 +352,6 @@ const popoverContentRef = ref();
</script>
<style scoped lang="scss">
@use "@styles/fa-table";
.reference :deep(.el-input__wrapper),
.reference :deep(.el-input__inner) {
cursor: pointer;
@@ -364,7 +362,7 @@ const popoverContentRef = ref();
justify-content: flex-end;
margin-top: 6px;
}
// 隐藏全选按钮
.radio :deep(.el-table__header th.el-table__cell:nth-child(1) .el-checkbox) {
visibility: hidden;
}
@@ -494,5 +494,119 @@ defineExpose({
</script>
<style lang="scss" scoped>
@use "@styles/fa-table";
.fa-table {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
.fa-table__main {
flex: 1;
min-height: 0;
padding-top: 10px;
/* VueDraggable 透传高度,确保 ElTable height: 100% 能正确解析 */
> * {
height: 100%;
}
}
.el-table {
height: 100%;
}
:deep(.el-loading-mask) {
z-index: 100;
background-color: var(--default-box-color) !important;
}
/* Loading 过渡动画 - 消失时淡出 */
.loading-fade-leave-active {
transition: opacity 0.3s ease-out;
}
.loading-fade-leave-to {
opacity: 0;
}
/* 空状态垂直居中 */
&.is-empty {
:deep(.el-table__body-wrapper) {
display: flex;
align-items: center;
justify-content: center;
}
}
.pagination {
display: flex;
flex-shrink: 0;
padding-top: 13px;
:deep(.el-select) {
width: 102px !important;
}
/* 分页对齐方式 */
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
/* 自定义分页组件样式 */
&.custom-pagination {
:deep(.el-pagination) {
.btn-prev,
.btn-next {
background-color: transparent;
border: 1px solid var(--fa-gray-300);
transition: border-color 0.15s;
&:hover:not(.is-disabled) {
color: var(--theme-color);
border-color: var(--theme-color);
}
}
li {
box-sizing: border-box;
font-weight: 400 !important;
background-color: transparent;
border: 1px solid var(--fa-gray-300);
transition: border-color 0.15s;
&.is-active {
font-weight: 400;
color: #fff;
background-color: var(--theme-color);
border: 1px solid var(--theme-color);
}
&:hover:not(.is-disabled) {
border-color: var(--theme-color);
}
}
}
}
}
}
/* 移动端分页 */
@media (width <= 640px) {
:deep(.el-pagination) {
display: flex;
flex-wrap: wrap;
gap: 15px 0;
align-items: center;
justify-content: center;
}
}
</style>
@@ -8,6 +8,7 @@
<script setup lang="ts">
import { ref, computed, watchEffect } from "vue";
import DOMPurify from "dompurify";
interface Props {
size?: string | number;
@@ -68,7 +69,7 @@ const loadSvgContent = async () => {
}
const content = await response.text();
svgContent.value = applyThemeToSvg(content);
svgContent.value = DOMPurify.sanitize(applyThemeToSvg(content));
} catch (error) {
console.error("Failed to load SVG:", error);
svgContent.value = "";
@@ -222,6 +222,6 @@ defineExpose({
});
</script>
<style scoped>
@import "@styles/fa-login.css";
<style scoped lang="scss">
@use "@styles/custom/fa-login";
</style>
@@ -111,6 +111,6 @@ defineExpose({
});
</script>
<style scoped>
@import "@styles/fa-login.css";
<style scoped lang="scss">
@use "@styles/custom/fa-login";
</style>
@@ -196,64 +196,5 @@ onBeforeUnmount(() => {
</script>
<style scoped lang="scss">
@import "@styles/fa-login.css";
.login-mobile-flow {
margin-top: 1rem;
}
.login-mobile-otp-cell {
box-sizing: border-box;
flex: 1;
min-width: 0;
max-width: 48px;
height: 40px;
padding: 0;
font-size: 1rem;
font-weight: 500;
color: var(--el-text-color-primary);
text-align: center;
appearance: none;
outline: none;
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color);
border-radius: 8px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
&:focus {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
}
.login-mobile-sms-btn {
border-radius: 8px;
}
html.dark {
.login-mobile-otp-cell {
background: rgb(255 255 255 / 6%);
border-color: rgb(255 255 255 / 14%);
&:focus {
border-color: var(--el-color-primary);
}
}
}
/* EP 全局规则:相邻 .el-button { margin-left: 12px },纵向叠放时仍会命中,导致第二颗按钮横向错位 */
.login-mobile-actions {
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
:deep(.el-button) {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
}
}
@use "@styles/custom/fa-login";
</style>
@@ -65,23 +65,5 @@ const qrPayload = computed(() => {
</script>
<style scoped lang="scss">
@import "@styles/fa-login.css";
.login-qr-flow {
margin-top: 1rem;
}
.login-qr-card {
box-sizing: border-box;
}
/* EP:相邻按钮横向间距;单列返回按钮无需处理 */
.login-mobile-actions {
:deep(.el-button) {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
}
}
@use "@styles/custom/fa-login";
</style>
@@ -133,6 +133,6 @@ defineExpose({
});
</script>
<style scoped>
@import "@styles/fa-login.css";
<style scoped lang="scss">
@use "@styles/custom/fa-login";
</style>
+9
View File
@@ -302,6 +302,15 @@
transform: none !important;
}
// 表格按钮悬浮效果
.hover-btn {
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
filter: brightness(1.1);
transform: translateY(-2px) scale(1.05);
}
}
// 去除移动端点击背景色
@media screen and (width <= 1180px) {
* {
-18
View File
@@ -1,18 +0,0 @@
// CRUD 页面通用样式
// 从各 module_system 视图文件中提取的重复样式
// FaForm 在 Dialog 中的布局优化:隐藏最后一列操作按钮、内容区域自适应宽度
.crud-dialog-art-form {
:deep(.el-row > .el-col:last-child) {
display: none;
}
:deep(.el-form-item__content) {
max-width: 100%;
}
}
// 表格操作列按钮垂直居中
:deep(.inline-flex) {
vertical-align: middle;
}
@@ -1,40 +0,0 @@
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
// 自定义Element 亮色主题
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"white": #ffffff,
"black": #000000,
"primary": (
"base": #4080FF,
),
"success": (
"base": #13deb9,
),
"warning": (
"base": #ffae1f,
),
"danger": (
"base": #ff4d4f,
),
"error": (
"base": #fa896b,
),
"info": (
"base": #909399,
),
),
$button: (
"hover-bg-color": var(--el-color-primary-light-9),
"hover-border-color": var(--el-color-primary),
"border-color": var(--el-color-primary),
"text-color": var(--el-color-primary),
),
$messagebox: (
"border-radius": "12px",
),
$popover: (
"padding": "14px",
"border-radius": "10px",
)
);
+41 -3
View File
@@ -55,7 +55,6 @@
-o-animation: $animate;
animation: $animate;
/* stylelint-disable-next-line at-rule-prelude-no-invalid */
@keyframes #{$name} {
from {
@each $key, $value in $from {
@@ -69,6 +68,20 @@
}
}
}
@keyframes #{$name} {
from {
@each $key, $value in $from {
$key: $value;
}
}
to {
@each $key, $value in $to {
$key: $value;
}
}
}
}
// 圆形盒子
@@ -83,13 +96,18 @@
// placeholder
@mixin placeholder($color: #bbb) {
// Firefox
&::placeholder {
color: $color;
opacity: 1;
}
// Safari and Chrome (legacy)
/* stylelint-disable-next-line selector-no-vendor-prefix */
// Internet Explorer 10+
&:-ms-input-placeholder {
color: $color;
}
// Safari and Chrome
&::-webkit-input-placeholder {
color: $color;
}
@@ -99,6 +117,26 @@
}
}
//背景透明,文字不透明。兼容IE8
@mixin betterTransparentize($color, $alpha) {
$c: rgba($color, $alpha);
$ie_c: ie_hex_str($c);
zoom: 1;
background: rgba($color, 1);
background: $c;
background: transparent \9;
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})";
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c});
}
//添加浏览器前缀
@mixin browserPrefix($propertyName, $value) {
@each $prefix in -webkit-, -moz-, -ms-, -o-, "" {
#{$prefix}#{$propertyName}: $value;
}
}
// 边框
@mixin border($color: red) {
border: 1px solid $color;
@@ -6,6 +6,10 @@ $bg-animation-duration: 0.5s;
html {
--bg-animation-color: #{$bg-animation-color-light};
&.dark {
--bg-animation-color: #{$bg-animation-color-dark};
}
// View transition styles
&::view-transition-old(*) {
animation: none;
@@ -24,8 +28,6 @@ html {
}
&.dark {
--bg-animation-color: #{$bg-animation-color-dark};
&::view-transition-old(*) {
animation: clip $bg-animation-duration ease-in reverse both;
}
@@ -0,0 +1,53 @@
/**
* Element Plus 功能性颜色覆写 CSS 变量。
*
* 默认主色请与 src/config/setting.ts 中 themeColorPresets 首项保持一致。
*
* 注意:primary 颜色由运行时 setElementThemeColor() 动态写入,此处只覆写
* success / warning / danger / error / info 等编译期确定的功能性颜色。
* 这些变量无法通过 SCSS `@forward ... with()` 传递到 Element Plus 组件编译链,
* 因为 Dart Sass 模块隔离机制下 Element Plus 组件内部 `@use 'common/var'` 获得的是独立模块实例。
*/
@use "sass:map";
@use "element-plus/theme-chalk/src/common/var.scss" as ep with (
$colors: (
"white": #ffffff,
"black": #000000,
"primary": (
"base": #4080ff,
),
"success": (
"base": #13deb9,
),
"warning": (
"base": #ffae1f,
),
"danger": (
"base": #ff4d4f,
),
"error": (
"base": #fa896b,
),
"info": (
"base": #909399,
),
)
);
:root {
@each $type in (success, warning, danger, error, info) {
--el-color-#{$type}: #{map.get(ep.$colors, $type, "base")} !important;
@each $i in (3, 5, 7, 8, 9) {
--el-color-#{$type}-light-#{$i}: #{map.get(ep.$colors, $type, "light-#{$i}")} !important;
}
--el-color-#{$type}-dark-2: #{map.get(ep.$colors, $type, "dark-2")} !important;
}
}
/** 暗黑主题 */
html.dark {
--el-color-primary: #{map.get(ep.$colors, "primary", "base")} !important;
}
@@ -0,0 +1,412 @@
/**
* 登录根布局
* - 顶栏 fixed 透明叠在页面上(不占一条实体顶区);仅插画列 / 表单区随布局切换
* - left / right:插画列与表单列横向排列(约 65% / 35%)
* - center:全宽 + 视口级背景;登录卡居中、阴影高亮
*/
.login-auth-split {
display: flex;
min-width: 0;
min-height: 0;
}
.login-auth-split__col--illustration {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
.login-auth-split__col--form {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
/* 横向:插画 ~65%,表单列占剩余 */
.login-auth-split--left,
.login-auth-split--right {
flex-direction: row;
}
.login-auth-split--left .login-auth-split__col--illustration,
.login-auth-split--right .login-auth-split__col--illustration {
flex: 0 0 65%;
}
.login-auth-split--left .login-auth-split__col--form,
.login-auth-split--right .login-auth-split__col--form {
flex: 1 1 0;
min-width: 0;
}
.login-auth-split--left {
flex-direction: row-reverse;
}
@media only screen and (width <= 1600px) {
.login-auth-split--left .login-auth-split__col--illustration,
.login-auth-split--right .login-auth-split__col--illustration {
flex: 0 0 60%;
}
}
/* 居中模式:仅一列表单壳占满屏宽 */
.login-auth-split--center {
flex-direction: column;
}
.login-auth-split--center .login-auth-split__col--form {
flex: 1 1 auto;
width: 100%;
min-height: 0;
background-color: transparent;
}
/* 顶栏与页脚之间的区域:纵向居中承载登录卡 */
.login-auth-split--center .login-page-panel__scroll.login-page-panel__scroll--centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 0;
padding-bottom: 0;
/* 盖过 Tailwind overflow-y-auto:仅卡片内滚动,外层不出滚动条 */
overflow: hidden;
}
.login-page-panel__scroll--centered .login-panel-align-row {
flex: 0 1 auto;
justify-content: center;
width: 100%;
min-height: 0;
max-height: 100%;
}
/* 悬浮卡片:居中模式尺寸与原先一致(520 宽、720/视口上限),无边框 + 主色高亮阴影 */
.login-auth-split--center .auth-right-wrap {
width: min(520px, calc(100vw - 2rem)) !important;
max-width: min(520px, calc(100vw - 2rem));
height: auto;
max-height: calc(100dvh - 6.5rem);
padding: 1.75rem 2rem 2rem;
overflow: hidden auto;
background-color: var(--el-bg-color);
border: none;
border-radius: 16px;
box-shadow:
0 6px 20px rgb(15 23 42 / 9%),
0 14px 36px rgb(15 23 42 / 10%),
0 0 18px color-mix(in srgb, var(--el-color-primary) 16%, transparent),
0 0 32px color-mix(in srgb, var(--el-color-primary) 7%, transparent);
}
.login-auth-split--center .auth-right-wrap .form {
height: auto;
min-height: 0;
padding-top: 1rem;
padding-bottom: 1rem;
}
html.dark .login-auth-split--center .auth-right-wrap {
border: none;
box-shadow:
0 8px 28px rgb(0 0 0 / 42%),
0 16px 44px rgb(0 0 0 / 46%),
0 0 22px color-mix(in srgb, var(--el-color-primary) 20%, transparent),
0 0 36px color-mix(in srgb, var(--el-color-primary) 9%, transparent);
}
/* 居中模式下页脚弱化,减少「分区后台」感 */
.login-page-footer--floating-layout {
border-top-color: color-mix(in srgb, var(--el-border-color-lighter) 55%, transparent);
}
html.dark .login-page-footer--floating-layout {
border-top-color: rgb(255 255 255 / 8%);
}
@media only screen and (width <= 1180px) {
.login-auth-split {
flex-direction: column !important;
}
.login-auth-split__col--illustration {
flex: 0 0 auto !important;
width: 100%;
}
.login-auth-split__col--form {
flex: 1 1 auto !important;
width: 100%;
min-height: 0;
}
/* 窄屏横排切竖排:恢复插画;居中模式本身无插画,不受影响 */
.login-auth-split--center .auth-right-wrap {
width: min(520px, calc(100vw - 1.5rem)) !important;
max-width: min(520px, calc(100vw - 1.5rem));
max-height: calc(100dvh - 6rem);
}
}
/* 授权页表单区域(左右分栏时) */
.auth-right-wrap {
position: relative;
flex-shrink: 0;
width: 440px;
height: 720px;
padding-top: 5px;
padding-bottom: 5px;
overflow: hidden;
animation: slideInRight 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
@media (width <= 640px) {
width: 100%;
height: auto;
max-height: calc(100vh - 10rem);
padding-right: 1.75rem;
padding-left: 1.75rem;
}
@media (width <= 768px) {
animation: none;
}
.form {
height: 100%;
padding-top: 40px;
padding-bottom: 40px;
}
.title {
font-size: 2.25rem;
font-weight: 600;
line-height: 2.5rem;
color: var(--fa-gray-900);
@media (width <= 768px) {
font-size: 1.875rem;
line-height: 2.25rem;
}
@media (width <= 640px) {
padding-top: 2.5rem;
}
}
.sub-title {
margin-top: 10px;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--fa-gray-600);
}
.custom-height {
height: 40px !important;
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.login-page-panel {
.btn {
border-radius: 8px;
}
}
html.dark .login-page-panel {
.text-g-800 {
color: rgb(255 255 255 / 78%) !important;
}
}
.login-page-form {
margin-top: 1rem;
}
.login-page-form .el-form-item {
margin-bottom: 1.1rem;
}
.el-select__wrapper {
min-height: 42px !important;
}
.login-secondary-actions .login-secondary-btn {
height: 42px;
border-radius: 8px;
}
html.dark {
.login-page-form .el-input__wrapper {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
.login-page-form .el-input__inner {
color: rgb(255 255 255 / 92%);
}
.login-page-form .el-input__inner::placeholder {
color: rgb(255 255 255 / 35%);
}
.login-page-form .el-select .el-select__wrapper {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
.login-page-form .el-select__placeholder {
color: rgb(255 255 255 / 40%);
}
.login-page-form .el-select__selected-item {
color: rgb(255 255 255 / 92%);
}
.login-remember .el-checkbox__label {
color: rgb(255 255 255 / 65%);
}
.login-secondary-btn {
color: rgb(255 255 255 / 85%) !important;
background: transparent !important;
border-color: rgb(255 255 255 / 22%) !important;
border-radius: 8px;
&:hover {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border-color: var(--el-color-primary) !important;
}
}
.login-secondary-actions .login-secondary-btn {
height: 42px;
}
}
.login-page-footer {
margin-top: 0;
text-align: center;
border-top: 1px solid var(--el-border-color-lighter);
.login-footer-text {
line-height: 1.65;
color: var(--el-text-color-secondary) !important;
}
.login-page-footer__link {
color: var(--el-text-color-regular);
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: var(--el-color-primary);
}
}
.login-page-footer__sep {
margin: 0 0.35rem;
color: var(--el-border-color);
}
.login-page-footer__record {
display: inline-block;
margin-left: 0.35rem;
color: var(--el-text-color-placeholder);
}
}
html.dark .login-page-footer {
border-top-color: rgb(255 255 255 / 8%);
.login-footer-text {
color: rgb(255 255 255 / 42%) !important;
}
.login-page-footer__link {
color: rgb(255 255 255 / 48%);
&:hover {
color: var(--el-color-primary);
}
}
.login-page-footer__sep {
color: rgb(255 255 255 / 25%);
}
.login-page-footer__record {
color: rgb(255 255 255 / 35%);
}
}
.login-mobile-otp-cell {
box-sizing: border-box;
flex: 1;
min-width: 0;
max-width: 48px;
height: 40px;
padding: 0;
font-size: 1rem;
font-weight: 500;
color: var(--el-text-color-primary);
text-align: center;
appearance: none;
outline: none;
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color);
border-radius: 8px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
&:focus {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
}
.login-mobile-sms-btn {
border-radius: 8px;
}
.login-mobile-actions {
.el-button + .el-button {
margin-left: 0 !important;
}
.el-button {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
}
}
.login-qr-card {
box-sizing: border-box;
}
html.dark {
.login-mobile-otp-cell {
background: rgb(255 255 255 / 6%);
border-color: rgb(255 255 255 / 14%);
&:focus {
border-color: var(--el-color-primary);
}
}
}
-189
View File
@@ -1,189 +0,0 @@
@reference '@styles/core/tailwind.css';
/**
* 登录根布局
* - 顶栏 fixed 透明叠在页面上(不占一条实体顶区);仅插画列 / 表单区随布局切换
* - left / right:插画列与表单列横向排列(约 65% / 35%)
* - center:全宽 + 视口级背景;登录卡居中、阴影高亮
*/
.login-auth-split {
display: flex;
min-width: 0;
min-height: 0;
}
.login-auth-split__col--illustration {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
.login-auth-split__col--form {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
/* 横向:插画 ~65%,表单列占剩余 */
.login-auth-split--left,
.login-auth-split--right {
flex-direction: row;
}
.login-auth-split--left .login-auth-split__col--illustration,
.login-auth-split--right .login-auth-split__col--illustration {
flex: 0 0 65%;
}
.login-auth-split--left .login-auth-split__col--form,
.login-auth-split--right .login-auth-split__col--form {
flex: 1 1 0;
min-width: 0;
}
.login-auth-split--left {
flex-direction: row-reverse;
}
@media only screen and (width <= 1600px) {
.login-auth-split--left .login-auth-split__col--illustration,
.login-auth-split--right .login-auth-split__col--illustration {
flex: 0 0 60%;
}
}
/* 居中模式:仅一列表单壳占满屏宽 */
.login-auth-split--center {
flex-direction: column;
}
.login-auth-split--center .login-auth-split__col--form {
flex: 1 1 auto;
width: 100%;
min-height: 0;
background-color: transparent;
}
/* 顶栏与页脚之间的区域:纵向居中承载登录卡 */
.login-auth-split--center .login-page-panel__scroll.login-page-panel__scroll--centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 0;
padding-bottom: 0;
/* 盖过 Tailwind overflow-y-auto:仅卡片内滚动,外层不出滚动条 */
overflow: hidden;
}
.login-page-panel__scroll--centered .login-panel-align-row {
flex: 0 1 auto;
justify-content: center;
width: 100%;
min-height: 0;
max-height: 100%;
}
/* 悬浮卡片:居中模式尺寸与原先一致(520 宽、720/视口上限),无边框 + 主色高亮阴影 */
.login-auth-split--center .auth-right-wrap {
width: min(520px, calc(100vw - 2rem)) !important;
max-width: min(520px, calc(100vw - 2rem));
height: auto;
max-height: calc(100dvh - 6.5rem);
padding: 1.75rem 2rem 2rem;
overflow: hidden auto;
background-color: var(--el-bg-color);
border: none;
border-radius: 16px;
box-shadow:
0 6px 20px rgb(15 23 42 / 9%),
0 14px 36px rgb(15 23 42 / 10%),
0 0 18px color-mix(in srgb, var(--el-color-primary) 16%, transparent),
0 0 32px color-mix(in srgb, var(--el-color-primary) 7%, transparent);
}
.login-auth-split--center .auth-right-wrap .form {
height: auto;
min-height: 0;
padding-top: 1rem;
padding-bottom: 1rem;
}
html.dark .login-auth-split--center .auth-right-wrap {
border: none;
box-shadow:
0 8px 28px rgb(0 0 0 / 42%),
0 16px 44px rgb(0 0 0 / 46%),
0 0 22px color-mix(in srgb, var(--el-color-primary) 20%, transparent),
0 0 36px color-mix(in srgb, var(--el-color-primary) 9%, transparent);
}
/* 居中模式下页脚弱化,减少「分区后台」感 */
.login-page-footer--floating-layout {
border-top-color: color-mix(in srgb, var(--el-border-color-lighter) 55%, transparent);
}
html.dark .login-page-footer--floating-layout {
border-top-color: rgb(255 255 255 / 8%);
}
@media only screen and (width <= 1180px) {
.login-auth-split {
flex-direction: column !important;
}
.login-auth-split__col--illustration {
flex: 0 0 auto !important;
width: 100%;
}
.login-auth-split__col--form {
flex: 1 1 auto !important;
width: 100%;
min-height: 0;
}
/* 窄屏横排切竖排:恢复插画;居中模式本身无插画,不受影响 */
.login-auth-split--center .auth-right-wrap {
width: min(520px, calc(100vw - 1.5rem)) !important;
max-width: min(520px, calc(100vw - 1.5rem));
max-height: calc(100dvh - 6rem);
}
}
/* 授权页表单区域(左右分栏时) */
.auth-right-wrap {
@apply relative w-[440px] shrink-0 h-[720px] py-[5px] overflow-hidden
max-sm:px-7 max-sm:w-full max-sm:h-auto max-sm:max-h-[calc(100vh-10rem)]
animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards]
max-md:animate-none;
.form {
@apply h-full py-[40px];
}
.title {
@apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10;
}
.sub-title {
@apply mt-[10px] text-g-600 text-sm;
}
.custom-height {
@apply !h-[40px];
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@@ -1,92 +0,0 @@
@use "@styles/core/mixin.scss" as *;
// 设置抽屉模态框样式
.setting-modal {
background: transparent !important;
.el-drawer {
// 背景滤镜效果
background: rgba($color: #fff, $alpha: 50%) !important;
box-shadow: 0 0 30px rgb(0 0 0 / 10%) !important;
@include backdropBlur();
.setting-box-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
width: calc(100% + 15px);
margin-bottom: 10px;
.setting-item {
box-sizing: border-box;
width: calc(33.333% - 15px);
margin-right: 15px;
text-align: center;
.box {
position: relative;
box-sizing: border-box;
display: flex;
height: 52px;
overflow: hidden;
cursor: pointer;
border: 2px solid var(--default-border);
border-radius: 8px;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%);
transition: box-shadow 0.1s;
&.mt-16 {
margin-top: 16px;
}
&.is-active {
border: 2px solid var(--theme-color);
}
img {
width: 100%;
height: 100%;
}
}
.name {
margin-top: 6px;
font-size: 14px;
text-align: center;
}
}
}
}
// 去除滚动条
.el-drawer__body::-webkit-scrollbar {
width: 0 !important;
}
}
.dark {
.setting-modal {
.el-drawer {
background: rgba($color: #000, $alpha: 50%) !important;
.setting-item {
.box {
border: 2px solid transparent;
}
}
}
}
}
// 去除火狐浏览器滚动条
:deep(.el-drawer__body) {
scrollbar-width: none;
}
// 移动端隐藏
@media screen and (width <= 800px) {
.mobile-hide {
display: none !important;
}
}
@@ -1,258 +0,0 @@
@use "@styles/core/mixin.scss" as *;
// 菜单样式变量
$menu-height: 42px;
$menu-icon-size: 20px;
$menu-font-size: 14px;
$hover-bg-color: var(--fa-gray-200);
$popup-menu-height: 40px;
$popup-menu-padding: 8px;
$popup-menu-margin: 5px;
$popup-menu-radius: 6px;
// 通用菜单项样式
@mixin menu-item-base {
width: calc(100% - 16px);
margin-left: 8px;
border-radius: 6px;
.menu-icon {
margin-left: -7px;
}
}
// 通用 hover 样式
@mixin menu-hover($bg-color) {
.el-sub-menu__title:hover,
.el-menu-item:not(.is-active):hover {
background: $bg-color !important;
}
}
// 通用选中样式
@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) {
.el-menu-item.is-active {
color: $color !important;
background-color: $bg-color;
.menu-icon {
.art-svg-icon {
color: $icon-color !important;
}
}
}
}
// 弹窗菜单项样式
@mixin popup-menu-item {
height: $popup-menu-height;
margin-bottom: $popup-menu-margin;
border-radius: $popup-menu-radius;
.menu-icon {
margin-right: 5px;
}
&:last-of-type {
margin-bottom: 0;
}
}
// 主题菜单通用样式(合并 design 和 dark 主题的共同逻辑)
@mixin theme-menu-base {
.el-sub-menu__title,
.el-menu-item {
@include menu-item-base;
}
}
// 弹窗菜单通用样式
@mixin popup-menu-base($hover-bg, $active-color, $active-bg) {
.el-menu--popup {
padding: $popup-menu-padding;
.el-sub-menu__title:hover,
.el-menu-item:hover {
background-color: $hover-bg !important;
border-radius: $popup-menu-radius;
}
.el-menu-item {
@include popup-menu-item;
&.is-active {
color: $active-color !important;
background-color: $active-bg !important;
}
}
.el-sub-menu {
@include popup-menu-item;
height: $popup-menu-height !important;
.el-sub-menu__title {
height: $popup-menu-height !important;
border-radius: $popup-menu-radius;
}
}
}
}
.layout-sidebar {
// ---------------------- Modify default style ----------------------
// 菜单折叠样式
.menu-left-close {
.header {
.logo {
margin: 0 auto;
}
}
}
// 菜单图标
.menu-icon {
margin-right: 8px;
font-size: $menu-icon-size;
}
// 菜单高度
.el-sub-menu__title,
.el-menu-item {
height: $menu-height !important;
margin-bottom: 4px;
line-height: $menu-height !important;
span {
font-size: $menu-font-size !important;
@include ellipsis();
}
}
// 右侧箭头
.el-sub-menu__icon-arrow {
width: 13px !important;
font-size: 13px !important;
}
// 菜单折叠
.el-menu--collapse {
.el-sub-menu.is-active {
.el-sub-menu__title {
.menu-icon {
.art-svg-icon {
// 选中菜单图标颜色
color: var(--theme-color) !important;
}
}
}
}
}
// ---------------------- Design theme menu ----------------------
.el-menu-design {
@include theme-menu-base;
@include menu-active(var(--theme-color), var(--el-color-primary-light-9));
@include menu-hover($hover-bg-color);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-600);
}
}
// ---------------------- Dark theme menu ----------------------
.el-menu-dark {
@include theme-menu-base;
@include menu-active(#fff, #27282d, #fff);
@include menu-hover(#0f1015);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-400);
}
}
// ---------------------- Light theme menu ----------------------
.el-menu-light {
.el-sub-menu__title,
.el-menu-item {
.menu-icon {
margin-left: 1px;
}
}
.el-menu-item.is-active {
background-color: var(--el-color-primary-light-9);
.art-svg-icon {
color: var(--theme-color) !important;
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
content: "";
background: var(--theme-color);
}
}
@include menu-hover($hover-bg-color);
.el-sub-menu__icon-arrow {
color: var(--fa-gray-600);
}
}
}
@media only screen and (width <= 640px) {
.layout-sidebar {
.el-menu-design {
> .el-sub-menu {
margin-left: 0;
}
.el-sub-menu {
width: 100% !important;
}
}
}
}
// 菜单折叠 hover 弹窗样式(浅色主题)
.el-menu--vertical,
.el-menu--popup-container {
@include popup-menu-base(var(--fa-gray-200), var(--fa-gray-900), var(--fa-gray-200));
}
// 暗黑模式菜单样式
.dark {
.el-menu--vertical,
.el-menu--popup-container {
@include popup-menu-base(var(--fa-gray-200), var(--fa-gray-900), #292a2e);
}
.layout-sidebar {
// 图标颜色、文字颜色
.menu-icon .art-svg-icon,
.menu-name {
color: var(--fa-gray-800) !important;
}
// 选中的文字颜色跟图标颜色
.el-menu-item.is-active {
span,
.menu-icon .art-svg-icon {
color: var(--theme-color) !important;
}
}
// 右侧箭头颜色
.el-sub-menu__icon-arrow {
color: #fff;
}
}
}
@@ -1,251 +0,0 @@
.layout-sidebar {
display: flex;
height: 100vh;
user-select: none;
scrollbar-width: none;
border-right: 1px solid var(--fa-card-border);
&.no-border {
border-right: none !important;
}
// 自定义滚动条宽度
:deep(.el-scrollbar__bar.is-vertical) {
width: 4px;
}
:deep(.el-scrollbar__thumb) {
right: -2px;
background-color: #ccc;
border-radius: 2px;
}
.dual-menu-left {
position: relative;
width: 80px;
height: 100%;
border-right: 1px solid var(--fa-card-border) !important;
transition: width 0.25s;
.logo {
margin: auto;
margin-top: 12px;
margin-bottom: 3px;
cursor: pointer;
}
ul {
li {
> div {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 8px;
overflow: hidden;
text-align: center;
cursor: pointer;
border-radius: 5px;
.art-svg-icon {
display: block;
margin: 0 auto;
font-size: 20px;
}
span {
display: -webkit-box;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
line-clamp: 1;
font-size: 12px;
-webkit-box-orient: vertical;
}
&.is-active {
background: var(--el-color-primary-light-9);
.art-svg-icon,
span {
color: var(--theme-color) !important;
}
}
}
}
}
.switch-btn {
position: absolute;
right: 0;
bottom: 15px;
left: 0;
margin: auto;
}
}
.menu-left {
position: relative;
box-sizing: border-box;
height: 100vh;
@media only screen and (width <= 640px) {
height: 100dvh;
}
.el-menu {
height: 100%;
}
&:hover {
.dual-menu-collapse-btn {
opacity: 1 !important;
}
}
.dual-menu-collapse-btn {
position: absolute;
top: 50%;
right: -11px;
z-index: 10;
width: 11px;
height: 50px;
cursor: pointer;
background-color: var(--default-box-color);
border: 1px solid var(--fa-card-border);
border-radius: 0 15px 15px 0;
opacity: 0;
transform: translateY(-50%);
transition: opacity 0.2s;
&:hover {
.art-svg-icon {
color: var(--fa-gray-800) !important;
}
}
.art-svg-icon {
position: absolute;
top: 0;
bottom: 0;
left: -4px;
margin: auto;
transition: all 0.3s;
}
}
}
.header {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
height: 60px;
overflow: hidden;
line-height: 60px;
cursor: pointer;
.logo {
margin-left: 22px;
}
p {
position: absolute;
top: 0;
bottom: 0;
left: 58px;
box-sizing: border-box;
margin-left: 10px;
font-size: 18px;
&.is-dual-menu-name {
left: 25px;
margin: auto;
}
}
}
.el-menu {
box-sizing: border-box;
// 防止菜单内的滚动影响整个页面滚动
overscroll-behavior: contain;
scrollbar-width: none;
border-right: 0;
-ms-scroll-chaining: contain;
&::-webkit-scrollbar {
width: 0 !important;
}
}
.menu-model {
display: none;
}
}
@media only screen and (width <= 800px) {
.layout-sidebar {
width: 0;
.header {
height: 50px;
line-height: 50px;
}
.el-menu {
height: calc(100vh - 60px);
}
.el-menu--collapse {
width: 0;
}
// 折叠状态下的header样式
.menu-left-close .header {
.logo {
display: none;
}
p {
left: 16px;
font-size: 0;
opacity: 0 !important;
}
}
.menu-model {
position: fixed;
top: 0;
left: 0;
z-index: -1;
display: block;
width: 100%;
height: 100vh;
background: rgba($color: #000, $alpha: 50%);
transition: opacity 0.2s ease-in-out;
}
}
}
@media only screen and (width <= 640px) {
.layout-sidebar {
border-right: 0 !important;
}
}
.dark {
.layout-sidebar {
border-right: 1px solid rgb(255 255 255 / 13%);
:deep(.el-scrollbar__thumb) {
background-color: #777;
}
.dual-menu-left {
border-right: 1px solid rgb(255 255 255 / 9%) !important;
}
}
}
-124
View File
@@ -1,124 +0,0 @@
.fa-table {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
.fa-table__main {
flex: 1;
min-height: 0;
padding-top: 10px;
// VueDraggable 透传高度,确保 ElTable height: 100% 能正确解析
> * {
height: 100%;
}
}
.el-table {
height: 100%;
}
:deep(.el-loading-mask) {
z-index: 100;
background-color: var(--default-box-color) !important;
}
// Loading 过渡动画 - 消失时淡出
.loading-fade-leave-active {
transition: opacity 0.3s ease-out;
}
.loading-fade-leave-to {
opacity: 0;
}
// 空状态垂直居中
&.is-empty {
:deep(.el-table__body-wrapper) {
display: flex;
align-items: center;
justify-content: center;
}
}
.pagination {
display: flex;
flex-shrink: 0;
padding-top: 13px;
:deep(.el-select) {
width: 102px !important;
}
// 分页对齐方式
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
// 自定义分页组件样式
&.custom-pagination {
:deep(.el-pagination) {
.btn-prev,
.btn-next {
background-color: transparent;
border: 1px solid var(--fa-gray-300);
transition: border-color 0.15s;
&:hover:not(.is-disabled) {
color: var(--theme-color);
border-color: var(--theme-color);
}
}
li {
box-sizing: border-box;
font-weight: 400 !important;
background-color: transparent;
border: 1px solid var(--fa-gray-300);
transition: border-color 0.15s;
&.is-active {
font-weight: 400;
color: #fff;
background-color: var(--theme-color);
border: 1px solid var(--theme-color);
}
&:hover:not(.is-disabled) {
border-color: var(--theme-color);
}
}
}
}
}
}
// 表格按钮悬浮效果
.hover-btn {
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
filter: brightness(1.1);
transform: translateY(-2px) scale(1.05);
}
}
// 移动端分页
@media (width <= 640px) {
:deep(.el-pagination) {
display: flex;
flex-wrap: wrap;
gap: 15px 0;
align-items: center;
justify-content: center;
}
}
-273
View File
@@ -1,273 +0,0 @@
$box-radius: calc(var(--custom-radius) / 3 + 2px);
// 全屏容器 z-index 调整
.w-e-full-screen-container {
z-index: 100 !important;
}
/* 编辑器容器 */
.editor-wrapper {
width: 100%;
height: 100%;
border: 1px solid var(--fa-gray-300);
border-radius: $box-radius !important;
.w-e-bar {
border-radius: $box-radius $box-radius 0 0 !important;
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
i {
margin-right: 5px;
}
}
/* 工具栏 */
.editor-toolbar {
border-bottom: 1px solid var(--default-border);
}
/* 下拉选择框配置 */
.w-e-select-list {
min-width: 140px;
padding: 5px 10px 10px;
border: none;
border-radius: $box-radius;
}
/* 下拉选择框元素配置 */
.w-e-select-list ul li {
margin-top: 5px;
font-size: 15px !important;
border-radius: $box-radius;
}
/* 下拉选择框 正文文字大小调整 */
.w-e-select-list ul li:last-of-type {
font-size: 16px !important;
}
/* 下拉选择框 hover 样式调整 */
.w-e-select-list ul li:hover {
background-color: var(--fa-gray-200);
}
:root {
/* 激活颜色 */
--w-e-toolbar-active-bg-color: var(--fa-gray-200);
/* toolbar 图标和文字颜色 */
--w-e-toolbar-color: #000;
/* 表格选中时候的边框颜色 */
--w-e-textarea-selected-border-color: #ddd;
/* 表格头背景颜色 */
--w-e-textarea-slight-bg-color: var(--fa-gray-200);
}
/* 工具栏按钮样式 */
.w-e-bar-item svg {
fill: var(--fa-gray-800);
}
.w-e-bar-item button {
color: var(--fa-gray-800);
border-radius: $box-radius;
}
/* 工具栏 hover 按钮背景颜色 */
.w-e-bar-item button:hover {
background-color: var(--fa-gray-200);
}
/* 工具栏分割线 */
.w-e-bar-divider {
height: 20px;
margin-top: 10px;
background-color: #ccc;
}
/* 工具栏菜单 */
.w-e-bar-item-group .w-e-bar-item-menus-container {
min-width: 120px;
padding: 10px 0;
border: none;
border-radius: $box-radius;
.w-e-bar-item {
button {
width: 100%;
margin: 0 5px;
}
}
}
/* 代码块 */
.w-e-text-container [data-slate-editor] pre > code {
padding: 0.6rem 1rem;
background-color: var(--fa-gray-50);
border-radius: $box-radius;
}
/* 弹出框 */
.w-e-drop-panel {
border: 0;
border-radius: $box-radius;
}
a {
color: #318ef4;
}
.w-e-text-container {
[data-slate-editor] {
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0.8em 0 0.4em;
font-weight: 700;
line-height: 1.35;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1.125em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: 0.875em;
}
ul,
ol {
padding-left: 1.5em;
margin: 0.8em 0;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li {
margin: 0.25em 0;
}
ul ul {
list-style: circle;
}
ul ul ul {
list-style: square;
}
}
strong,
b {
font-weight: 700;
}
i,
em {
font-style: italic;
}
}
/* 表格样式优化 */
.w-e-text-container [data-slate-editor] .table-container th {
border-right: none;
}
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
border-right: 1px solid #ccc !important;
}
/* 引用 */
.w-e-text-container [data-slate-editor] blockquote {
background-color: var(--fa-gray-200);
border-left: 4px solid var(--fa-gray-300);
}
/* 输入区域弹出 bar */
.w-e-hover-bar {
border-radius: $box-radius;
}
/* 超链接弹窗 */
.w-e-modal {
border: none;
border-radius: $box-radius;
}
/* 图片样式调整 */
.w-e-text-container [data-slate-editor] .w-e-selected-image-container {
overflow: inherit;
&:hover {
border: 0;
}
img {
border: 1px solid transparent;
transition: border 0.3s;
&:hover {
border: 1px solid #318ef4 !important;
}
}
.w-e-image-dragger {
width: 12px;
height: 12px;
background-color: #318ef4;
border: 2px solid #fff;
border-radius: $box-radius;
}
.left-top {
top: -6px;
left: -6px;
}
.right-top {
top: -6px;
right: -6px;
}
.left-bottom {
bottom: -6px;
left: -6px;
}
.right-bottom {
right: -6px;
bottom: -6px;
}
}
}
+3 -21
View File
@@ -7,6 +7,9 @@
// Element Plus 样式优化
@use "./core/el-ui";
// Element Plus 功能性颜色覆写 CSS 变量
@use "./core/variables";
// Element Plus 暗黑主题
@use "./core/el-dark";
@@ -21,24 +24,3 @@
// 主题切换圆形扩散动画
@use "./core/theme-animation";
// CRUD 页面通用样式
@use "./core/crud";
// 布局组件样式
@use "./layouts";
// 侧边栏菜单样式
@use "./fa-sidebar-menu";
// 侧边栏菜单主题样式
@use "./fa-sidebar-menu-theme";
// 设置面板样式
@use "./fa-settings-panel";
// 表格组件样式
@use "./fa-table";
// 富文本编辑器样式
@use "./fa-wang-editor";
-8
View File
@@ -1,8 +0,0 @@
/**
* 全局 SCSS 变量(由 vite.config.ts → css.preprocessorOptions.scss.additionalData 注入到每个 `.vue` 的 `lang="scss"`
*
* 默认主色请与 `src/config/setting.ts` 中 `themeColorPresets` 首项、主题切换逻辑保持一致。
*/
// 供组件内 `$primary-base` 等引用(可选)
$primary-base: #4080ff !default;
+1 -2
View File
@@ -9,11 +9,10 @@ import { StorageConfig } from "@utils";
import { BANNER } from "../../../build/banner";
// -----------------------------
// Console banner
// Console bannerANSI 转义码生成网站 https://patorjk.com/software/taag/#p=testall&f=Fire+Font-k&t=fastapiadmin%0A&x=none&v=4&h=4&w=80&we=false
// -----------------------------
export function printConsoleBanner(): void {
const asciiArt = `
\x1b[32m欢迎使用 ${StorageConfig.appName}-v${StorageConfig.CURRENT_VERSION}
\x1b[0m
+5 -4
View File
@@ -265,13 +265,14 @@ export const getTabConfig = (style: string) =>
// -----------------------------
// EmojiText (default export-compatible)
// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈']
// -----------------------------
export const EmojiText: { [key: string]: string } = {
"0": "O_O",
"200": "^_^",
"400": "T_T",
"500": "X_X",
"0": "O_O", // 空
"200": "^_^", // 成功
"400": "T_T", // 错误请求
"500": "X_X", // 服务器内部错误,无法完成请求
};
// -----------------------------
@@ -25,9 +25,7 @@
留言讨论
</ElButton>
<ElButton @click="visible = false">关闭</ElButton>
<ElButton v-if="articleId != null" v-auth="'edit'" type="primary" @click="onEditClick">
编辑
</ElButton>
<ElButton v-if="articleId != null" type="primary" @click="onEditClick"> 编辑 </ElButton>
</div>
</template>
</ElDrawer>
@@ -37,6 +35,7 @@
import "@styles/core/md.scss";
import "@styles/custom/one-dark-pro.scss";
import axios from "axios";
import DOMPurify from "dompurify";
defineOptions({ name: "ArticleDetailDrawer" });
@@ -91,7 +90,7 @@ const fetchDetail = async () => {
if (data.code === 200) {
articleTitle.value = data.data.title;
articleHtml.value = data.data.html_content;
articleHtml.value = DOMPurify.sanitize(data.data.html_content);
} else {
error.value = "文章加载失败";
}
@@ -27,7 +27,7 @@
<FaSvgIcon icon="ri:message-3-line" class="mr-1" />
留言墙
</ElButton>
<ElButton type="primary" @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
<ElButton type="primary" @click="toAddArticle">新增文章</ElButton>
</ElCol>
</ElRow>
@@ -65,7 +65,6 @@
</div>
<ElButton
class="opacity-0 group-hover:opacity-100"
v-auth="'edit'"
size="small"
@click.stop="toEdit(item)"
>
@@ -670,144 +670,6 @@ async function submitForget() {
}
</script>
<style scoped>
@import "@styles/fa-login.css";
</style>
<style lang="scss" scoped>
.login-page-panel {
:deep(.btn) {
border-radius: 8px;
}
}
html.dark .login-page-panel {
:deep(.text-g-800) {
color: rgb(255 255 255 / 78%) !important;
}
}
/* 副标题与表单块间距(login-page-form 在子组件内,须 :deep */
:deep(.login-page-form) {
margin-top: 1rem;
}
:deep(.login-page-form .el-form-item) {
margin-bottom: 1.1rem;
}
:deep(.el-select__wrapper) {
min-height: 42px !important;
}
/* 仅账号登录底部「手机号 / 扫码」双列按钮需要 42px;手机号登录页的返回钮与主按钮同为 h-11,勿全局压高度 */
:deep(.login-secondary-actions .login-secondary-btn) {
height: 42px;
border-radius: 8px;
}
html.dark {
:deep(.login-page-form .el-input__wrapper) {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
:deep(.login-page-form .el-input__inner) {
color: rgb(255 255 255 / 92%);
}
:deep(.login-page-form .el-input__inner::placeholder) {
color: rgb(255 255 255 / 35%);
}
:deep(.login-page-form .el-select .el-select__wrapper) {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
:deep(.login-page-form .el-select__placeholder) {
color: rgb(255 255 255 / 40%);
}
:deep(.login-page-form .el-select__selected-item) {
color: rgb(255 255 255 / 92%);
}
:deep(.login-remember .el-checkbox__label) {
color: rgb(255 255 255 / 65%);
}
:deep(.login-secondary-btn) {
color: rgb(255 255 255 / 85%) !important;
background: transparent !important;
border-color: rgb(255 255 255 / 22%) !important;
border-radius: 8px;
&:hover {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border-color: var(--el-color-primary) !important;
}
}
:deep(.login-secondary-actions .login-secondary-btn) {
height: 42px;
}
}
.login-page-footer {
margin-top: 0;
text-align: center;
border-top: 1px solid var(--el-border-color-lighter);
.login-footer-text {
line-height: 1.65;
color: var(--el-text-color-secondary) !important;
}
.login-page-footer__link {
color: var(--el-text-color-regular);
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: var(--el-color-primary);
}
}
.login-page-footer__sep {
margin: 0 0.35rem;
color: var(--el-border-color);
}
.login-page-footer__record {
display: inline-block;
margin-left: 0.35rem;
color: var(--el-text-color-placeholder);
}
}
html.dark .login-page-footer {
border-top-color: rgb(255 255 255 / 8%);
.login-footer-text {
color: rgb(255 255 255 / 42%) !important;
}
.login-page-footer__link {
color: rgb(255 255 255 / 48%);
&:hover {
color: var(--el-color-primary);
}
}
.login-page-footer__sep {
color: rgb(255 255 255 / 25%);
}
.login-page-footer__record {
color: rgb(255 255 255 / 35%);
}
}
<style scoped lang="scss">
@use "@styles/custom/fa-login";
</style>
@@ -170,6 +170,7 @@ import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelec
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import { ElTag, ElMessage } from "element-plus";
import DOMPurify from "dompurify";
defineOptions({
name: "Notice",
@@ -307,9 +308,12 @@ const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue")
{ label: "更新时间", prop: "updated_time" },
];
/** 详情富文本 HTML(用于预览) */
/** 详情富文本 HTML(用于预览,已做 XSS 净化 */
const detailContentHtml = computed({
get: () => detailFormData.value.notice_content ?? "",
get: () => {
const raw = detailFormData.value.notice_content ?? "";
return DOMPurify.sanitize(raw);
},
set: (v: string) => {
detailFormData.value.notice_content = v;
},
@@ -112,7 +112,7 @@
</template>
<template #reply_content>
<div v-if="detailFormData.reply" class="ticket-html-preview">
<div v-html="detailFormData.reply" />
<div v-html="sanitizedReply" />
</div>
<p v-else class="ticket-html-empty">暂无回复</p>
</template>
@@ -217,6 +217,7 @@ import FaForm from "@/components/forms/fa-form/index.vue";
import FaWangEditor from "@/components/forms/fa-wang-editor/index.vue";
import { ElTag, ElMessage, ElSelect, ElOption, ElRadioGroup, ElRadio } from "element-plus";
import { h } from "vue";
import DOMPurify from "dompurify";
defineOptions({
name: "Ticket",
@@ -384,14 +385,23 @@ const ticketDetailItems: import("@/components/others/fa-descriptions/index.vue")
{ label: "更新时间", prop: "updated_time" },
];
/** 详情富文本 HTML(用于预览) */
/** 详情富文本 HTML(用于预览,已做 XSS 净化 */
const detailContentHtml = computed({
get: () => detailFormData.value.ticket_content ?? "",
get: () => {
const raw = detailFormData.value.ticket_content ?? "";
return DOMPurify.sanitize(raw);
},
set: (v: string) => {
detailFormData.value.ticket_content = v;
},
});
/** 回复富文本 HTML(已做 XSS 净化) */
const sanitizedReply = computed(() => {
const raw = detailFormData.value.reply ?? "";
return raw ? DOMPurify.sanitize(raw) : "";
});
/** 详情是否有可视文本 */
const detailHasRenderableContent = computed(() => {
const raw = detailFormData.value.ticket_content ?? "";
@@ -18,7 +18,7 @@
/>
<ElCard
class="flex flex-1 min-h-0 flex-col fa-table-card"
class="flex flex-1 min-h-0 flex-col fa-table-card job-page-card"
:style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
>
<FaTableHeader
@@ -29,30 +29,43 @@
@refresh="refreshJobList"
>
<template #left>
<div class="status-content">
<span>调度器监控</span>
<div class="status-item">
<span class="label">状态</span>
<ElTag :type="getSchedulerStatusType(schedulerStatus.status)" size="large">
{{ schedulerStatus.status }}
</ElTag>
<div class="scheduler-inline">
<div class="scheduler-metrics">
<div class="scheduler-metric">
<span class="scheduler-metric__label">调度器</span>
<ElTag
:type="getSchedulerStatusType(schedulerStatus.status)"
size="small"
effect="dark"
>
{{ schedulerStatus.status }}
</ElTag>
</div>
<ElDivider direction="vertical" />
<div class="scheduler-metric">
<span class="scheduler-metric__label">运行中</span>
<ElTag
:type="schedulerStatus.is_running ? 'success' : 'info'"
size="small"
effect="dark"
>
{{ schedulerStatus.is_running ? "是" : "否" }}
</ElTag>
</div>
<ElDivider direction="vertical" />
<div class="scheduler-metric">
<span class="scheduler-metric__label">任务</span>
<span class="scheduler-metric__count">{{ schedulerStatus.job_count }}</span>
</div>
</div>
<div class="status-item">
<span class="label">运行中</span>
<ElTag :type="schedulerStatus.is_running ? 'success' : 'danger'" size="large">
{{ schedulerStatus.is_running ? "是" : "否" }}
</ElTag>
</div>
<div class="status-item">
<span class="label">任务数量</span>
<ElTag type="warning" size="large">{{ schedulerStatus.job_count }}</ElTag>
</div>
<div class="status-actions">
<ElDivider direction="vertical" />
<div class="scheduler-actions">
<ElButton
v-hasPerm="['module_task:cronjob:job:scheduler']"
type="success"
icon="VideoPlay"
:disabled="schedulerStatus.status !== '停止'"
size="small"
@click="handleStartScheduler"
>
启动
@@ -62,6 +75,7 @@
type="warning"
icon="VideoPause"
:disabled="schedulerStatus.status !== '运行中'"
size="small"
@click="handlePauseScheduler"
>
暂停
@@ -71,6 +85,7 @@
type="primary"
icon="RefreshRight"
:disabled="schedulerStatus.status !== '暂停'"
size="small"
@click="handleResumeScheduler"
>
恢复
@@ -80,15 +95,19 @@
type="danger"
icon="SwitchButton"
:disabled="schedulerStatus.status === '停止'"
size="small"
@click="handleShutdownScheduler"
>
关闭
</ElButton>
<ElDivider direction="vertical" />
<ElButton
v-hasPerm="['module_task:cronjob:job:task']"
type="danger"
icon="Delete"
:disabled="schedulerStatus.job_count === 0"
size="small"
plain
@click="handleClearAllJobs"
>
清空任务
@@ -97,6 +116,7 @@
v-hasPerm="['module_task:cronjob:job:query']"
type="info"
icon="Monitor"
size="small"
@click="handleOpenConsole"
>
控制台
@@ -105,24 +125,18 @@
v-hasPerm="['module_task:cronjob:job:scheduler']"
type="primary"
icon="Refresh"
size="small"
plain
@click="handleSyncJobs"
>
同步
</ElButton>
<ElButton
v-hasPerm="['module_task:cronjob:job:update']"
type="warning"
icon="Refresh"
@click="refreshJobList"
>
刷新
</ElButton>
</div>
</div>
</template>
</FaTableHeader>
<div v-loading="jobLoading" class="job-cards-container min-h-0 flex-1 overflow-auto">
<div v-loading="jobLoading" class="job-cards-container mt-3 min-h-0 flex-1 overflow-auto">
<ElEmpty
v-if="!jobLoading && (!jobList || jobList.length === 0)"
:image-size="80"
@@ -130,123 +144,99 @@
/>
<ElRow v-else :gutter="16">
<ElCol
v-for="(job, index) in jobList"
v-for="job in jobList"
:key="job.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
:md="6"
:lg="4"
class="job-card-col"
>
<ElCard class="job-card" shadow="hover">
<ElCard shadow="hover" :class="`job-card job-card--${getJobStatusClass(job.status)}`">
<template #header>
<div class="job-card-header">
<div class="job-card-title">
<span class="job-index">{{ index + 1 }}</span>
<span class="job-name" :title="job.name">{{ job.name }}</span>
</div>
<ElTag :type="getJobStatusType(job.status)" size="small">
<div class="job-card-title">
<span
class="job-card-dot"
:class="`job-card-dot--${getJobStatusClass(job.status)}`"
/>
<span class="job-card-name" :title="job.name">{{ job.name }}</span>
<ElTag :type="getJobStatusType(job.status)" size="small" effect="dark">
{{ getJobStatusLabel(job.status) }}
</ElTag>
</div>
</template>
<div class="job-card-body">
<div class="job-info-item">
<span class="job-info-label">任务ID:</span>
<span class="job-info-value">{{ job.id }}</span>
<div class="job-card-body-row">
<FaSvgIcon :icon="getTriggerIcon(job.trigger)" class="job-card-meta-icon" />
<span class="job-card-meta-text">{{ formatTrigger(job.trigger) }}</span>
</div>
<div class="job-info-item">
<span class="job-info-label">触发器:</span>
<ElTag
v-if="String(job.trigger ?? '').includes('cron')"
type="primary"
size="small"
class="job-trigger-tag"
>
<ElIcon><Clock /></ElIcon>
{{ formatTrigger(job.trigger) }}
</ElTag>
<ElTag
v-else-if="String(job.trigger ?? '').includes('interval')"
type="success"
size="small"
class="job-trigger-tag"
>
<ElIcon><Timer /></ElIcon>
{{ formatTrigger(job.trigger) }}
</ElTag>
<ElTag
v-else-if="String(job.trigger ?? '').includes('date')"
type="warning"
size="small"
class="job-trigger-tag"
>
<ElIcon><Calendar /></ElIcon>
{{ formatTrigger(job.trigger) }}
</ElTag>
<ElTag v-else type="info" size="small" class="job-trigger-tag">
{{ formatTrigger(job.trigger) }}
</ElTag>
</div>
<div class="job-info-item">
<span class="job-info-label">下次执行:</span>
<span class="job-info-value">{{ job.next_run_time || "无" }}</span>
<div class="job-card-body-row">
<FaSvgIcon icon="ri:time-line" class="job-card-meta-icon" />
<span class="job-card-meta-text">{{ job.next_run_time || "暂无" }}</span>
</div>
</div>
<template #footer>
<div class="job-card-footer">
<ElButton
v-if="job.status === '暂停中'"
v-hasPerm="['module_task:cronjob:job:task']"
type="primary"
size="small"
icon="VideoPlay"
@click="handleResumeJob(job.id)"
>
恢复
</ElButton>
<ElButton
v-if="job.status === '运行中'"
v-hasPerm="['module_task:cronjob:job:task']"
type="warning"
size="small"
icon="VideoPause"
@click="handlePauseJob(job.id)"
>
暂停
</ElButton>
<ElButton
v-if="job.status !== '已停止' && job.status !== '未知'"
v-hasPerm="['module_task:cronjob:job:task']"
type="success"
size="small"
icon="CaretRight"
@click="handleRunJobNow(job.id)"
>
调试
</ElButton>
<ElButton
v-if="job.status !== '未知'"
v-hasPerm="['module_task:cronjob:job:task']"
type="danger"
size="small"
icon="Close"
@click="handleRemoveJob(job.id)"
>
移除
</ElButton>
<ElButton
v-hasPerm="['module_task:cronjob:job:query']"
type="info"
size="small"
icon="List"
@click="handleOpenExecutionLogDrawer(job)"
>
记录
</ElButton>
</div>
<ElRow :gutter="8">
<ElCol :span="6">
<ElButton
v-hasPerm="['module_task:cronjob:job:task']"
:type="job.status === '暂停中' ? 'primary' : 'warning'"
size="small"
plain
class="w-full"
:icon="job.status === '暂停中' ? 'VideoPlay' : 'VideoPause'"
:disabled="job.status !== '暂停中' && job.status !== '运行中'"
@click="
job.status === '暂停中' ? handleResumeJob(job.id) : handlePauseJob(job.id)
"
>
{{ job.status === "暂停中" ? "恢复" : "暂停" }}
</ElButton>
</ElCol>
<ElCol :span="6">
<ElButton
v-hasPerm="['module_task:cronjob:job:task']"
type="success"
size="small"
plain
class="w-full"
icon="CaretRight"
:disabled="job.status === '已停止' || job.status === '未知'"
@click="handleRunJobNow(job.id)"
>
调试
</ElButton>
</ElCol>
<ElCol :span="6">
<ElButton
v-hasPerm="['module_task:cronjob:job:query']"
type="info"
size="small"
plain
class="w-full"
icon="List"
@click="handleOpenExecutionLogDrawer(job)"
>
记录
</ElButton>
</ElCol>
<ElCol :span="6">
<ElButton
v-hasPerm="['module_task:cronjob:job:task']"
type="danger"
size="small"
plain
class="w-full"
icon="Close"
:disabled="job.status === '未知'"
@click="handleRemoveJob(job.id)"
>
移除
</ElButton>
</ElCol>
</ElRow>
</template>
</ElCard>
</ElCol>
@@ -362,8 +352,7 @@ import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vu
import FaJsonPretty from "@/components/others/fa-json-pretty/index.vue";
import { useTable } from "@/hooks/core/useTable";
import type { ColumnOption } from "@/types/component";
import { Calendar, Clock, Timer } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { ElDivider, ElMessage, ElMessageBox, ElTag } from "element-plus";
import { computed, h, nextTick, onMounted, ref } from "vue";
import { Terminal, TerminalApi } from "vue-web-terminal";
@@ -996,6 +985,27 @@ function getTriggerTypeLabel(type: string | undefined) {
return map[type || ""] || type || "-";
}
function getJobStatusClass(status: string) {
switch (status) {
case "运行中":
return "running";
case "暂停中":
return "paused";
case "已停止":
return "stopped";
default:
return "unknown";
}
}
function getTriggerIcon(trigger: string | undefined) {
const t = (trigger ?? "").toLowerCase();
if (t.includes("cron")) return "ri:timer-line";
if (t.includes("interval")) return "ri:repeat-line";
if (t.includes("date")) return "ri:calendar-event-line";
return "ri:flashlight-line";
}
function getLogStatusType(status: string): "primary" | "success" | "warning" | "info" | "danger" {
const map: Record<string, "primary" | "success" | "warning" | "info" | "danger"> = {
pending: "info",
@@ -1033,110 +1043,165 @@ function handleViewJobState(row: JobLogTable) {
}
</script>
<style scoped>
<style scoped lang="scss">
.job-page :deep(.data-table) {
height: 100%;
}
.status-content {
display: flex;
gap: 24px;
align-items: center;
}
.status-item {
display: flex;
gap: 8px;
align-items: center;
}
.status-item .label {
font-weight: 500;
}
.status-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.terminal-wrapper {
height: 500px;
}
.job-card-header {
.scheduler-inline {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 4px;
align-items: center;
justify-content: space-between;
.el-divider--vertical {
height: 18px;
margin: 0 2px;
}
}
.scheduler-metrics {
display: flex;
gap: 4px;
align-items: center;
}
.scheduler-metric {
display: flex;
gap: 4px;
align-items: center;
white-space: nowrap;
&__label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
&__count {
font-size: 14px;
font-weight: 700;
color: var(--el-color-warning);
}
}
.scheduler-actions {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
.el-divider--vertical {
height: 18px;
margin: 0 2px;
}
}
.job-page-card {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
}
.job-card-col {
margin-bottom: 16px;
}
.job-card {
transition: box-shadow 0.25s;
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 6%);
}
:deep(.el-card__header) {
padding: 8px 14px;
}
:deep(.el-card__body) {
padding: 8px 14px;
}
:deep(.el-card__footer) {
padding: 8px 14px;
}
}
.job-card-title {
display: flex;
flex: 1;
gap: 8px;
gap: 6px;
align-items: center;
min-width: 0;
}
.job-index {
display: inline-flex;
.job-card-dot {
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: linear-gradient(
135deg,
var(--el-color-primary) 0%,
var(--el-color-primary-dark-2) 100%
);
width: 8px;
height: 8px;
border-radius: 50%;
&--running {
background: var(--el-color-success);
}
&--paused {
background: var(--el-color-warning);
}
&--stopped {
background: var(--el-color-danger);
}
&--unknown {
background: var(--el-color-info);
}
}
.job-name {
.job-card-body {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
overflow: hidden;
&-row {
display: flex;
gap: 4px;
align-items: center;
min-width: 0;
overflow: hidden;
}
}
.job-card-name {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 500;
font-weight: 600;
white-space: nowrap;
}
.job-info-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
}
.job-info-item:last-child {
margin-bottom: 0;
}
.job-info-label {
.job-card-meta-icon {
flex-shrink: 0;
width: 70px;
font-size: 13px;
color: var(--el-color-primary-light-3);
}
.job-info-value {
flex: 1;
.job-card-meta-text {
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.job-card-footer {
display: flex;
justify-content: space-between;
}
.job-card-footer .el-button {
flex: 1;
}
.execution-log-drawer {
height: 100%;
}
@@ -932,7 +932,8 @@ onMounted(async () => {
});
</script>
<style scoped>
<!-- popover 挂载到 body需单独写修复 vue3-cron-plus 全局 .el-tag--info { margin-left: -60px } 误伤多选下拉里 tag -->
<style scoped lang="scss">
.code-editor-container {
display: flex;
flex-direction: column;
@@ -1004,10 +1005,7 @@ onMounted(async () => {
padding-right: 10px;
padding-left: 10px;
}
</style>
<!-- popover 挂载到 body需单独写修复 vue3-cron-plus 全局 .el-tag--info { margin-left: -60px } 误伤多选下拉里 tag -->
<style lang="scss">
.node-cron-popover-fix {
.vue3-cron-plus-container .el-select .el-tag {
margin-left: 0 !important;
@@ -6,16 +6,16 @@
@mouseleave="showHandles = false"
>
<div class="node-content">
<span class="node-label">{{ data.label }}</span>
<span v-if="data.config && Object.keys(data.config).length > 0" class="node-badge">
{{ Object.keys(data.config).length }}
<span class="node-label">{{ nodeData.label }}</span>
<span v-if="nodeData.config && Object.keys(nodeData.config).length > 0" class="node-badge">
{{ Object.keys(nodeData.config).length }}
</span>
</div>
<Handle
v-if="nodeType.code !== 'input'"
:id="'top-' + id"
type="target"
position="top"
:position="Position.Top"
:class="{ 'handle-visible': showHandles }"
:style="{ background: nodeType.color || '#3b82f6' }"
/>
@@ -23,7 +23,7 @@
v-if="nodeType.code !== 'input'"
:id="'left-' + id"
type="target"
position="left"
:position="Position.Left"
:class="{ 'handle-visible': showHandles }"
:style="{ background: nodeType.color || '#3b82f6' }"
/>
@@ -31,7 +31,7 @@
v-if="nodeType.code !== 'output'"
:id="'right-' + id"
type="source"
position="right"
:position="Position.Right"
:class="{ 'handle-visible': showHandles }"
:style="{ background: nodeType.color || '#3b82f6' }"
/>
@@ -39,16 +39,16 @@
v-if="nodeType.code !== 'output'"
:id="'bottom-' + id"
type="source"
position="bottom"
:position="Position.Bottom"
:class="{ 'handle-visible': showHandles }"
:style="{ background: nodeType.color || '#3b82f6' }"
/>
</div>
</template>
<script setup>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { Handle } from "@vue-flow/core";
import { Handle, Position } from "@vue-flow/core";
const props = defineProps({
id: String,
@@ -58,36 +58,40 @@ const props = defineProps({
const showHandles = ref(false);
const nodeData = computed(
() => props.data ?? { label: "", config: {}, type: undefined, category: undefined }
);
const nodeType = computed(() => {
return {
code: props.data?.type || "custom",
name: props.data?.label || "自定义节点",
color: getCategoryColor(props.data?.category),
code: nodeData.value?.type || "custom",
name: nodeData.value?.label || "自定义节点",
color: getCategoryColor(nodeData.value?.category),
};
});
function getCategoryColor(category) {
function getCategoryColor(category: string | undefined) {
const colorMap = {
trigger: "#e6a23c",
action: "#409eff",
condition: "#67c23a",
control: "#909399",
};
return colorMap[category] || "#409eff";
return colorMap[category as keyof typeof colorMap] || "#409eff";
}
const nodeClass = computed(() => {
if (props.data?.type === "input") {
if (nodeData.value?.type === "input") {
return "start-node";
}
if (props.data?.type === "output") {
if (nodeData.value?.type === "output") {
return "end-node";
}
return "custom-node";
});
</script>
<style lang="scss">
<style scoped lang="scss">
.vue-flow__node-input {
display: flex !important;
align-items: center !important;
@@ -33,7 +33,7 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { ElButton, ElColorPicker, ElMessage, ElIcon } from "element-plus";
import { Close } from "@element-plus/icons-vue";
@@ -156,7 +156,7 @@ function handleDelete() {
}
</script>
<style scoped>
<style scoped lang="scss">
.edge-config-panel {
display: flex;
flex-direction: column;
@@ -207,7 +207,7 @@ onMounted(() => {
});
</script>
<style scoped>
<style scoped lang="scss">
.node-config-panel {
display: flex;
flex-direction: column;
@@ -114,7 +114,7 @@
<script lang="ts" setup>
defineOptions({
name: "WorkflowDefinition",
name: "Workflow",
inheritAttrs: false,
});
@@ -124,7 +124,7 @@ import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"
import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaWorkflowDesignDrawer from "../components/FaWorkflowDesignDrawer.vue";
import FaWorkflowDesignDrawer from "./components/FaWorkflowDesignDrawer.vue";
import { useTable } from "@/hooks/core/useTable";
import type { ColumnOption } from "@/types/component";
import { ArrowDown } from "@element-plus/icons-vue";
@@ -521,12 +521,4 @@ async function submitForm() {
}
</script>
<style scoped lang="scss">
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
<style scoped lang="scss"></style>