Browse Source

feat: 增加自定义主题颜色,调整默认暗色主题配色 (#6964)

pull/6979/head
2 weeks ago committed by GitHub
parent
commit
ffdd9b9d79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      frontend/src/assets/images/menu-bg.svg
  2. 4
      frontend/src/components/app-status/index.vue
  3. 5
      frontend/src/components/license-import/index.vue
  4. 5
      frontend/src/components/router-button/index.vue
  5. 38
      frontend/src/components/system-upgrade/index.vue
  6. 16
      frontend/src/components/v-charts/components/Pie.vue
  7. 31
      frontend/src/hooks/use-theme.ts
  8. 4
      frontend/src/layout/components/AppFooter.vue
  9. 2
      frontend/src/layout/components/Sidebar/components/Collapse.vue
  10. 1
      frontend/src/layout/components/Sidebar/components/Logo.vue
  11. 6
      frontend/src/layout/components/Sidebar/index.vue
  12. 2
      frontend/src/layout/index.vue
  13. 2
      frontend/src/store/interface/index.ts
  14. 8
      frontend/src/store/modules/global.ts
  15. 5
      frontend/src/styles/common.scss
  16. 492
      frontend/src/styles/element-dark.scss
  17. 41
      frontend/src/styles/element.scss
  18. 8
      frontend/src/utils/theme.ts
  19. 59
      frontend/src/utils/theme/tool.ts
  20. 22
      frontend/src/utils/xpack.ts
  21. 2
      frontend/src/views/home/app/index.vue
  22. 2
      frontend/src/views/host/terminal/terminal/index.vue
  23. 31
      frontend/src/views/login/components/login-form.vue
  24. 8
      frontend/src/views/setting/license/index.vue
  25. 88
      frontend/src/views/setting/panel/index.vue
  26. 226
      frontend/src/views/setting/panel/theme-color/index.vue

1
frontend/src/assets/images/menu-bg.svg

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' width='180' height='100' fill="currentColor" viewBox='0 0 180 100'><defs><clipPath id='clipPath'><rect x='0' y='0' width='180' height='100' rx='0' /></clipPath><linearGradient id='gradient1' x1='0.74' y1='1.14' x2='0.31' y2='0.11'><stop offset='0%' stop-color='var(--gradient-start-color, #FFFFFF)' stop-opacity='0' /><stop offset='100%' stop-color='var(--panel-gradient-end-color)' stop-opacity='1' /></linearGradient><linearGradient id='gradient2' x1='0.73' y1='-0.17' x2='0.43' y2='0.98'><stop offset='0%' stop-color='var(--gradient-start-color, #FFFFFF)' stop-opacity='0' /><stop offset='100%' stop-color='var(--panel-gradient-end-color)' stop-opacity='1' /></linearGradient></defs><g clip-path='url(#clipPath)'><rect x='0' y='0' width='180' height='100' rx='0' fill='#FFFFFF' fill-opacity='0' /><g><path d='M83,20.38C83,41.88,72.48,65.8,57.5,80.14C42.52,94.47,23.07,99.22,0,100.04V0H80.15C82.05,6.65,83,13.5,83,20.38Z' fill='url(#gradient1)' /></g><g><path d='M180.08,0.08V23.7C178.84,23.89,177.59,23.98,176.33,23.98C163.22,23.98,152.58,13.95,152.58,1.57Q152.58,0.82,152.64,0.08H180.08Z' fill='url(#gradient2)' /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

4
frontend/src/components/app-status/index.vue

@ -88,9 +88,9 @@
<LayoutContent :title="getTitle(key)" :divider="true">
<template #main>
<div class="app-warn">
<div class="flex flex-col gap-4 items-cetner justify-center w-full sm:flex-row">
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
<div>{{ $t('app.checkInstalledWarn', [data.app]) }}</div>
<span @click="goRouter(key)" class="flex items-cetner justify-center">
<span @click="goRouter(key)" class="flex items-center justify-center gap-0.5">
<el-icon class="flex items-center justify-center"><Position /></el-icon>
{{ $t('database.goInstall') }}
</span>

5
frontend/src/components/license-import/index.vue

@ -48,7 +48,7 @@ import { UploadFileData } from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { useTheme } from '@/hooks/use-theme';
import { getXpackSetting } from '@/utils/xpack';
import { getXpackSetting, initFavicon } from '@/utils/xpack';
const globalStore = GlobalStore();
const { switchTheme } = useTheme();
@ -90,10 +90,11 @@ const submit = async () => {
globalStore.isProductPro = true;
const xpackRes = await getXpackSetting();
if (xpackRes) {
globalStore.themeConfig.isGold = xpackRes.data.theme === 'dark-gold';
globalStore.themeConfig.themeColor = xpackRes.data.themeColor;
}
loading.value = false;
switchTheme();
initFavicon();
uploadRef.value!.clearFiles();
uploaderFiles.value = [];
open.value = false;

5
frontend/src/components/router-button/index.vue

@ -94,8 +94,9 @@ onMounted(() => {
}
.el-radio-button__original-radio:checked + .el-radio-button__inner {
color: $primary-color;
border-color: $primary-color !important;
color: var(--panel-button-text-color) !important;
background-color: var(--panel-button-bg-color) !important;
border-color: var(--panel-button-active) !important;
border-radius: 4px;
}
}

38
frontend/src/components/system-upgrade/index.vue

@ -1,7 +1,7 @@
<template>
<div>
<div class="flex w-full flex-col gap-2 md:flex-row">
<div class="flex flex-wrap" v-if="props.footer">
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
<div class="flex flex-wrap items-center" v-if="props.footer">
<el-button type="primary" link @click="toForum">
<span class="font-normal">{{ $t('setting.forum') }}</span>
</el-button>
@ -15,17 +15,17 @@
</el-button>
<el-divider v-if="!mobile" direction="vertical" />
</div>
<div class="flex flex-wrap">
<el-button type="primary" link @click="toHalo">
<span class="font-normal">
{{ isProductPro ? $t('license.pro') : $t('license.community') }}
</span>
</el-button>
<span class="version" @click="copyText(version)">{{ version }}</span>
<el-badge is-dot style="margin-top: -3px" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
<el-button type="primary" link @click="onLoadUpgradeInfo">
<span class="font-normal">({{ $t('setting.hasNewVersion') }})</span>
</el-button>
<div class="flex flex-wrap items-center">
<el-link :underline="false" type="primary" @click="toHalo">
{{ isProductPro ? $t('license.pro') : $t('license.community') }}
</el-link>
<el-link :underline="false" class="version" type="primary" @click="copyText(version)">
{{ version }}
</el-link>
<el-badge is-dot class="-mt-0.5" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
<el-link :underline="false" type="primary" @click="onLoadUpgradeInfo">
{{ $t('setting.hasNewVersion') }}
</el-link>
</el-badge>
<el-button
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
@ -33,7 +33,7 @@
link
@click="onLoadUpgradeInfo"
>
<span>({{ $t('setting.upgradeCheck') }})</span>
{{ $t('setting.upgradeCheck') }}
</el-button>
<el-tag v-if="version === 'Waiting'" round style="margin-left: 10px">
{{ $t('setting.upgrading') }}
@ -201,11 +201,10 @@ onMounted(() => {
<style lang="scss" scoped>
.version {
font-size: 14px;
color: var(--dark-gold-base-color);
color: var(--panel-color-primary-light-4);
text-decoration: none;
letter-spacing: 0.5px;
cursor: pointer;
margin-top: 2px;
}
.line-height {
line-height: 25px;
@ -221,10 +220,13 @@ onMounted(() => {
font-size: 14px;
}
:deep(.default-theme h2) {
color: var(--dark-gold-base-color);
margin: 13px, 0;
color: var(--el-color-primary);
margin: 13px 0;
padding: 0;
font-size: 16px;
}
}
:deep(.el-link__inner) {
font-weight: 400;
}
</style>

16
frontend/src/components/v-charts/components/Pie.vue

@ -7,7 +7,7 @@ import * as echarts from 'echarts';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { isDarkGoldTheme, isDarkTheme } = storeToRefs(globalStore);
const { isDarkTheme } = storeToRefs(globalStore);
const props = defineProps({
id: {
@ -25,7 +25,7 @@ const props = defineProps({
option: {
type: Object,
required: true,
}, // option: { title , data }
},
});
function initChart() {
@ -34,6 +34,12 @@ function initChart() {
myChart = echarts.init(document.getElementById(props.id) as HTMLElement);
}
let percentText = String(props.option.data).split('.');
const primaryLight2 = getComputedStyle(document.documentElement)
.getPropertyValue('--panel-color-primary-light-3')
.trim();
const primaryLight1 = getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim();
const pieBgColor = getComputedStyle(document.documentElement).getPropertyValue('--panel-pie-bg-color').trim();
const option = {
title: [
{
@ -99,11 +105,11 @@ function initChart() {
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: isDarkGoldTheme.value ? '#836c4c' : 'rgba(81, 192, 255, .1)',
color: primaryLight2,
},
{
offset: 1,
color: isDarkGoldTheme.value ? '#eaba63' : '#4261F6',
color: primaryLight1,
},
]),
],
@ -119,7 +125,7 @@ function initChart() {
label: {
show: false,
},
color: isDarkTheme.value ? '#16191D' : '#fff',
color: pieBgColor,
data: [
{
value: 0,

31
frontend/src/hooks/use-theme.ts

@ -1,22 +1,29 @@
import { GlobalStore } from '@/store';
import { setPrimaryColor } from '@/utils/theme';
export const useTheme = () => {
const globalStore = GlobalStore();
const switchTheme = () => {
if (globalStore.themeConfig.isGold && globalStore.isProductPro) {
const body = document.documentElement as HTMLElement;
body.setAttribute('class', 'dark-gold');
return;
const globalStore = GlobalStore();
const themeConfig = globalStore.themeConfig;
let itemTheme = themeConfig.theme;
if (itemTheme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
itemTheme = prefersDark ? 'dark' : 'light';
}
document.documentElement.className = itemTheme === 'dark' ? 'dark' : 'light';
if (globalStore.isProductPro && themeConfig.themeColor) {
try {
const themeColor = JSON.parse(themeConfig.themeColor);
const color = itemTheme === 'dark' ? themeColor.dark : themeColor.light;
let itemTheme = globalStore.themeConfig.theme;
if (globalStore.themeConfig.theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
itemTheme = prefersDark.matches ? 'dark' : 'light';
if (color) {
themeConfig.primary = color;
setPrimaryColor(color);
}
} catch (e) {
console.error('Failed to parse themeColor', e);
}
}
const body = document.documentElement as HTMLElement;
if (itemTheme === 'dark') body.setAttribute('class', 'dark');
else body.setAttribute('class', '');
};
return {

4
frontend/src/layout/components/AppFooter.vue

@ -28,8 +28,8 @@ const mobile = computed(() => {
align-items: center;
justify-content: space-between;
height: 48px;
background: #ffffff;
border-top: 1px solid #e4e7ed;
background: var(--panel-footer-bg);
border-top: 1px solid var(--panel-footer-border);
box-sizing: border-box;
padding: 10px 20px;
a {

2
frontend/src/layout/components/Sidebar/components/Collapse.vue

@ -18,7 +18,7 @@ const isCollapse = computed(() => menuStore.isCollapse);
display: flex;
align-items: center;
box-sizing: border-box;
border-top: 1px solid #e4e7ed;
border-top: 1px solid var(--panel-footer-border);
height: 48px;
}

1
frontend/src/layout/components/Sidebar/components/Logo.vue

@ -37,6 +37,7 @@ const goHome = () => {
align-items: center;
justify-content: center;
height: 55px;
z-index: 1;
img {
object-fit: contain;
width: 95%;

6
frontend/src/layout/components/Sidebar/index.vue

@ -6,6 +6,9 @@
element-loading-svg-view-box="-10, -10, 50, 50"
element-loading-background="rgba(122, 122, 122, 0.01)"
>
<div class="fixed">
<PrimaryMenu />
</div>
<Logo :isCollapse="isCollapse" />
<el-scrollbar>
<el-menu
@ -46,6 +49,7 @@ import { GlobalStore, MenuStore } from '@/store';
import { MsgSuccess } from '@/utils/message';
import { isString } from '@vueuse/core';
import { getSettingInfo } from '@/api/modules/setting';
import PrimaryMenu from '@/assets/images/menu-bg.svg?component';
const route = useRoute();
const menuStore = MenuStore();
@ -183,7 +187,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
height: 100%;
background: url(@/assets/images/menu-bg.png) var(--el-menu-bg-color) no-repeat top;
background: var(--panel-menu-bg-color) no-repeat top;
.el-scrollbar {
flex: 1;

2
frontend/src/layout/index.vue

@ -137,7 +137,7 @@ onMounted(() => {
height: 100vh;
transition: margin-left 0.3s;
margin-left: var(--panel-menu-width);
background-color: #f4f4f4;
background-color: var(--panel-main-bg-color-9);
overflow-x: hidden;
}
.app-main {

2
frontend/src/store/interface/index.ts

@ -4,13 +4,13 @@ export interface ThemeConfigProp {
panelName: string;
primary: string;
theme: string; // dark | bright | auto
isGold: boolean;
footer: boolean;
title: string;
logo: string;
logoWithText: string;
favicon: string;
themeColor: string;
}
export interface GlobalState {

8
frontend/src/store/modules/global.ts

@ -14,11 +14,10 @@ const GlobalStore = defineStore({
language: '',
themeConfig: {
panelName: '',
primary: '#005EEB',
primary: '#005eeb',
theme: 'auto',
isGold: false,
footer: true,
themeColor: '',
title: '',
logo: '',
logoWithText: '',
@ -46,9 +45,8 @@ const GlobalStore = defineStore({
getters: {
isDarkTheme: (state) =>
state.themeConfig.theme === 'dark' ||
state.themeConfig.isGold ||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
isDarkGoldTheme: (state) => state.themeConfig.isGold && state.isProductPro,
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
},
actions: {
setOpenMenuTabs(openMenuTabs: boolean) {

5
frontend/src/styles/common.scss

@ -124,7 +124,7 @@ html {
.input-help {
font-size: 12px;
word-break: break-all;
color: #8f959e;
color: #ADB0BC;
width: 100%;
display: inline-block;
}
@ -219,7 +219,6 @@ html {
background: var(--el-button-bg-color, var(--el-fill-color-blank));
border: 0;
font-weight: 350;
border-left: 0;
color: var(--el-button-text-color, var(--el-text-color-regular));
text-align: center;
box-sizing: border-box;
@ -361,7 +360,7 @@ html {
.el-input-group__append {
border-left: 0;
background-color: #ffffff !important;
background-color: var(--el-fill-color-light) !important;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset,

492
frontend/src/styles/element-dark.scss

@ -1,375 +1,237 @@
html.dark {
--el-box-shadow-light: 0px 0px 4px rgba(0, 0, 0, 0.1) !important;
--dark-gold-base-color: #5a5a5a;
--el-border-color-lighter: #1d2023;
--el-fill-color-blank: #111417;
--el-bg-color: rgba(0, 11, 21, 1);
// --el-text-color-primary: #999999;
--el-text-color-regular: #bbbfc4 !important;
--el-fill-color-light: #111417;
--el-border-color: #303438;
--el-bg-color-overlay: rgba(0, 11, 21, 1);
--el-border-color-light: #1d2023;
// * menu
--el-menu-bg-color: #111417 !important;
--el-menu-item-bg-color: #111417;
--el-menu-text-color: #ffffff;
--el-menu-item-bg-color-active: rgb(44, 45, 46);
// * panel-admin
--panel-text-color: rgb(174, 166, 153);
--panel-border: 1px solid #1d2023;
--panel-border-color: #394c5e;
--panel-main-bg-color: rgba(12, 12, 12, 1);
--panel-button-active: var(--el-color-primary);
--panel-login-shadow-light: 5px 5px 15px rgb(255 255 255 / 20%);
--panel-box-shadow-light: 0 0 10px rgb(255 255 255 / 10%);
--panel-popup-color: #060708;
--panel-alert-bg: #2f3030;
--panel-path-bg: #2f3030;
--panel-button-disabled: #5a5a5a;
.el-tag.el-tag--info {
--el-tag-bg-color: rgb(49, 51, 51);
--el-tag-border-color: rgb(64, 67, 67);
--panel-color-primary: #3D8EFF;
--panel-color-primary-light-8: #3674CC;
--panel-color-primary-light-1: #6EAAFF;
--panel-color-primary-light-2: #366FC2;
--panel-color-primary-light-3: #3364AD;
--panel-color-primary-light-4: #2F558F;
--panel-color-primary-light-5: #372E46;
--panel-color-primary-light-6: #2A4066;
--panel-color-primary-light-7: #2D4A7A;
--panel-color-primary-light-9: #2D4A7A;
--panel-main-bg-color-1: #E3E6F3;
--panel-main-bg-color-2: #C0C2CF;
--panel-main-bg-color-3: #ADB0BC;
--panel-main-bg-color-4: #9597A4;
--panel-main-bg-color-5: #90929F;
--panel-main-bg-color-6: #787B88;
--panel-main-bg-color-7: #5B5E6A;
--panel-main-bg-color-8: #434552;
--panel-main-bg-color-9: #2E313D;
--panel-main-bg-color-10: #242633;
--panel-main-bg-color-11: #60626F;
--panel-alert-error-bg-color: #54293A;
--panel-alert-error-text-color: #B22F48;
--panel-alert-error-hover-bg-color: #672A3D;
--panel-alert-success-bg-color: #1E5146;
--panel-alert-success-text-color: #169262;
--panel-alert-success-hover-bg-color: #1D5849;
--panel-alert-warning-bg-color: #59472A;
--panel-alert-warning-text-color: #BB8A2E;
--panel-alert-warning-hover-bg-color: #6A5531;
--panel-alert-info-bg-color: var(--panel-main-bg-color-6);
--panel-alert-info-text-color: var(--panel-main-bg-color-3);
--panel-alert-info-hover-bg-color: var(--panel-main-bg-color-4);
--panel-pie-bg-color: #434552;
--panel-text-color-white: #ffffff;
--el-color-primary: var(--panel-color-primary);
--el-color-primary-light-3: var(--panel-color-primary-light-3);
--el-color-primary-light-5: var(--panel-color-primary-light-5);
--el-color-primary-light-7: var(--panel-color-primary-light-7);
--el-color-primary-light-8: var(--panel-color-primary-light-8);
--el-color-primary-light-9: var(--panel-color-primary-light-9);
--panel-border: 2px solid var(--panel-main-bg-color-8);
--panel-button-active: var(--panel-main-bg-color-10);
--panel-button-text-color: var(--panel-main-bg-color-10);
--panel-button-bg-color: var(--panel-color-primary);
--panel-footer-bg: var(--panel-main-bg-color-9);
--panel-footer-border: var(--panel-main-bg-color-7);
--panel-text-color: var(--panel-main-bg-color-1);
--panel-menu-bg-color: var(--panel-main-bg-color-10);
--panel-terminal-tag-bg-color: var(--panel-main-bg-color-10);
--el-menu-item-bg-color: var(--panel-main-bg-color-10);
--el-menu-item-bg-color-active: var(--panel-main-bg-color-8);
--el-menu-hover-bg-color: var(--panel-main-bg-color-8);
--el-menu-text-color: var(--panel-main-bg-color-2);
--el-fill-color-blank: var(--panel-main-bg-color-10);
--el-fill-color-light: var(--panel-main-bg-color-10);
--el-border-color: var(--panel-main-bg-color-8);
--el-border-color-light: var(--panel-main-bg-color-8);
--el-border-color-lighter: var(--panel-main-bg-color-8);
--el-text-color-primary: var(--panel-main-bg-color-2);
--el-text-color-regular: var(--panel-main-bg-color-2);
--el-box-shadow: 0px 12px 32px 4px rgba(36, 38, 51, .36), 0px 8px 20px rgba(36, 38, 51, .72);
--el-box-shadow-light: 0px 0px 12px rgba(36, 38, 51, .72);
--el-box-shadow-lighter: 0px 0px 6px rgba(36, 38, 51, .72);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(36, 38, 51, .72), 0px 12px 32px #242633, 0px 8px 16px -8px #242633;
--el-bg-color: var(--panel-main-bg-color-9);
--el-bg-color-overlay: var(--panel-main-bg-color-9);
--el-text-color-placeholder: var(--panel-main-bg-color-4);
.el-descriptions__content:not(.is-bordered-label) {
color: var(--panel-main-bg-color-3);
}
.el-tag.el-tag--light {
--el-tag-bg-color: #111417;
--el-tag-border-color: var(--el-color-primary);
.el-menu-item:hover, .el-sub-menu__title:hover{
background: var(--panel-main-bg-color-8) !important;
}
.el-tag.el-tag--success {
--el-tag-border-color: var(--el-color-success);
.el-menu .el-menu-item {
box-shadow: 0 0 4px rgba(36, 38, 51, .72);
}
.el-tag.el-tag--danger {
--el-tag-border-color: var(--el-color-danger);
.el-menu .el-sub-menu__title {
box-shadow: 0 0 4px rgba(36, 38, 51, .72);
}
.el-card {
--el-card-bg-color: rgb(35, 35, 35);
color: #ffffff;
border: 1px solid var(--el-card-border-color) !important;
.el-overlay {
background-color: #2E313D90;
}
.el-table {
--el-table-bg-color: rgba(0, 11, 21, 1);
--el-table-tr-bg-color: rgba(0, 11, 21, 1);
--el-table-header-bg-color: rgba(0, 11, 21, 1);
--el-table-border: var(--panel-border);
--el-table-border-color: rgb(64, 67, 67);
.el-tag.el-tag--primary {
--el-tag-bg-color: var(--panel-main-bg-color-9);
--el-tag-border-color: var(--panel-color-primary-light-8);
--el-tag-hover-color: var(--panel-color-primary);
}
.el-message-box {
--el-messagebox-title-color: var(--el-menu-text-color);
border: 1px solid var(--panel-border-color);
.el-tabs--card > .el-tabs__header .el-tabs__nav {
border: 1px solid var(--panel-main-bg-color-8);
}
.el-alert--info {
--el-alert-bg-color: rgb(56, 59, 59);
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
border-bottom-color: var(--panel-color-primary);
--el-text-color-regular: var(--panel-color-primary);
}
.el-loading-mask {
background-color:var(--panel-main-bg-color-10);
}
.el-input {
--el-input-bg-color: rgb(47 48 48);
--el-input-border-color: #303438;
--el-input-border-color: var(--panel-main-bg-color-8);
}
.el-pagination {
--el-pagination-button-color: #999999;
input:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--el-box-shadow) inset;
background-color: var(--panel-main-bg-color-1);
transition: background-color 1000s ease-out 0.5s;
}
.el-popover {
--el-popover-title-text-color: #999999;
border: 1px solid var(--panel-border-color);
.el-input.is-disabled .el-input__wrapper {
--el-disabled-bg-color: var(--panel-main-bg-color-9);
--el-disabled-border-color: var(--panel-main-bg-color-8);
}
.md-editor-dark {
--md-bk-color: #111417;
.el-input > .el-input-group__append:hover {
background-color: var(--panel-main-bg-color-9) !important;
}
// * 以下为自定义暗黑模式内容
// login
.login-container {
.login-form {
input:-webkit-autofill {
box-shadow: 0 0 0 1000px #f1f4f9 inset;
-webkit-text-fill-color: #333333;
-webkit-transition: background-color 1000s ease-out 0.5s;
transition: background-color 1000s ease-out 0.5s;
}
.el-input__wrapper {
background-color: #ffffff;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
.el-input__inner {
color: #606266;
}
}
.el-form-item__label {
color: var(--panel-main-bg-color-3);
}
// scroll-bar
::-webkit-scrollbar {
background-color: var(--el-scrollbar-bg-color) !important;
.el-card {
--el-card-bg-color: var(--panel-main-bg-color-10)
}
::-webkit-scrollbar-thumb {
background-color: var(--el-border-color-darker);
.el-button--primary {
--el-button-hover-link-text-color: var(--panel-color-primary-light-1);
}
// sidebar
.sidebar-container-popper {
border: 1px solid #66686c;
.el-menu--popup-container {
border: none;
}
.el-button--primary.is-plain, .el-button--primary.is-text, .el-button--primary.is-link {
--el-button-bg-color: var(--panel-main-bg-color-9);
--el-button-border-color: var(--panel-main-bg-color-8);
--el-button-hover-bg-color: var(--panel-main-bg-color-9);
--el-button-hover-border-color: var(--panel-main-bg-color-8);
}
.sidebar-container {
border-right: 1px solid var(--el-border-color-light);
.el-button:hover {
--el-button-hover-text-color: var(--panel-text-color-white);
--el-button-border-color: var(--el-color-primary-light-3);
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-hover-border-color: var(--el-color-primary-light-3);
}
.el-menu {
.el-menu-item {
&:hover {
background: rgba(37, 39, 44, 1);
}
&.is-active {
background: var(--el-color-primary);
color: #ffffff;
&:hover {
.el-icon {
color: #ffffff !important;
}
span {
color: #ffffff !important;
}
}
&::before {
background: #ffffff;
}
}
}
.el-sub-menu {
.el-sub-menu__title {
&:hover {
background: rgba(37, 39, 44, 1);
}
}
}
.el-button:active {
--el-button-hover-text-color: var(--panel-text-color-white);
--el-button-active-bg-color: var(--el-color-primary-light-3);
--el-button-active-border-color: var(--el-color-primary-light-3);
}
.menu-collapse {
color: var(--el-menu-text-color);
border: var(--panel-border);
.el-button:focus-visible {
outline: none;
}
// layout
.app-wrapper {
.main-container {
background-color: var(--panel-main-bg-color) !important;
}
.app-footer {
background-color: var(--panel-main-bg-color) !important;
border-top: var(--panel-border);
}
.mobile-header {
background-color: var(--panel-main-bg-color) !important;
border-bottom: var(--panel-border);
color: #ffffff;
}
.el-button.is-disabled {
color: var(--panel-main-bg-color-7);
border-color: var(--panel-main-bg-color-8);
background: var(--panel-main-bg-color-9);
}
.system-label {
color: var(--el-menu-text-color);
.el-button.is-disabled:hover {
border-color: var(--panel-main-bg-color-8);
background: var(--panel-main-bg-color-9);
}
.router_card_button {
.el-radio-button__inner {
background: none !important;
}
.el-radio-button__original-radio:checked + .el-radio-button__inner {
color: #ffffff;
background-color: var(--panel-button-active) !important;
box-shadow: none !important;
border: none !important;
}
.el-button--primary.is-link.is-disabled {
color: var(--panel-main-bg-color-8);
}
// content-box
.content-box {
.text {
color: var(--el-text-color-regular) !important;
}
.el-dropdown-menu__item:hover {
background-color: var(--panel-main-bg-color-7);
}
.el-drawer .el-drawer__header span {
color: var(--el-menu-text-color);
}
.el-drawer {
border-left: 0.5px solid var(--panel-border-color);
}
.el-input__wrapper {
background-color: var(--el-disabled-bg-color);
}
.el-input.is-disabled .el-input__wrapper {
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
}
.el-radio-button__inner {
background: #1d2023;
}
.el-button--primary.is-plain.is-disabled {
background: #1d2023;
border-color: #303438;
}
.el-button--primary.is-plain {
background: #1d2023;
border-color: #303438;
}
.el-button.is-link.is-disabled {
color: var(--panel-button-disabled) !important;
}
.el-button.is-disabled {
border-color: #303438;
color: var(--panel-button-disabled);
}
.el-popper.is-dark {
color: rgb(171 173 173);
}
.path {
border: var(--panel-border);
.split {
color: #666666;
}
}
input:-webkit-autofill {
box-shadow: 0 0 0 1000px #323232 inset;
-webkit-text-fill-color: #cfd3dc;
transition: background-color 1000s ease-out 0.5s;
}
.el-avatar {
--el-avatar-bg-color: #111417 !important;
box-shadow: 0px 0px 4px rgba(0, 94, 235, 0.1);
border: 0.5px solid #1f2022;
}
.el-page-header__content {
color: rgb(174, 166, 153);
}
.el-dialog {
background-color: var(--panel-main-bg-color-8);
border: 1px solid var(--panel-border-color);
.el-dialog__header {
border-bottom: var(--panel-border);
color: #999999;
color: var(--el-text-color-primary);
.el-dialog__title {
color: var(--el-menu-text-color);
}
}
}
.el-tabs__item {
color: #999999;
}
.el-tabs__item.is-active {
color: var(--el-color-primary) !important;
}
.el-descriptions__title {
color: #999999;
}
.el-descriptions__content.el-descriptions__cell.is-bordered-content {
color: #999999;
}
.el-descriptions--large .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell {
padding: 12px 15px;
background-color: transparent;
}
.el-descriptions__body {
background-color: transparent;
}
.el-descriptions__label {
color: var(--el-color-primary) !important;
margin-right: 16px;
}
.terminal-tabs {
background: none !important;
.el-tabs__header {
background: #000000;
}
}
.el-pager {
li {
color: #999999;
&.is-active {
color: var(--el-pagination-hover-color);
}
}
}
.el-loading-mask {
background-color: rgba(0, 0, 0, 0.8);
}
.h-app-card {
.h-app-content {
.h-app-title {
color: #f2f8ff;
}
}
.el-alert--error {
--el-alert-bg-color: var(--panel-alert-error-bg-color);
--el-color-error: var(--panel-alert-error-text-color);
}
.infinite-list .infinite-list-item {
background: #212426;
&:hover {
background: #212426;
}
.el-alert--success {
--el-alert-bg-color: var(--panel-alert-success-bg-color);
--el-color-success: var(--panel-alert-success-text-color);;
}
.el-alert--warning.is-light {
background-color: rgb(56, 59, 59);
color: var(--el-color-warning);
}
.el-dropdown-menu__item.is-disabled {
color: var(--panel-button-disabled);
}
.el-date-editor .el-range-separator {
color: var(--panel-button-disabled);
.el-alert--warning {
--el-alert-bg-color: var(--panel-alert-warning-bg-color);
--el-color-warning: var(--panel-alert-warning-text-color);
}
.el-input-group__append {
border-left: 0;
background-color: #212426 !important;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset,
-1px 0 0 0 var(--el-input-border-color) inset;
&:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9) !important;
}
}
.el-collapse-item__header {
color: #ffffff;
}
.file-item:hover {
background-color: #4f4f4f;
.el-alert--info {
--el-alert-bg-color: var(--panel-alert-info-bg-color);
--el-color-info: var(--panel-alert-info-text-color);
}
.level-up-pro {
--dark-gold-base-color: #eaba63;
--dark-gold-text-color: #1f2329;
--el-color-primary: var(--dark-gold-base-color);
.title {
color: var(--dark-gold-base-color);
}
.el-button--primary {
&.is-plain {
background: var(--dark-gold-base-color);
--el-button-text-color: var(--dark-gold-text-color);
&.is-disabled {
background: #1d2023;
border-color: #303438;
}
}
&.is-text {
--el-button-text-color: var(--dark-gold-base-color);
}
}
}
}
}

41
frontend/src/styles/element.scss

@ -1,33 +1,56 @@
:root {
--el-color-primary: #005eeb;
--el-color-primary-dark-2: #0054d3;
--panel-gradient-end-color: var(--el-color-primary-light-7);
--el-color-primary-light-1: #196eed;
--el-color-primary-light-2: #337eef;
--el-color-primary-light-3: #4c8ef1;
--el-color-primary-light-4: #669ef3;
--el-color-primary-light-5: #7faef5;
--el-color-primary-light-6: #99bef7;
--el-color-primary-light-7: #b2cef9;
--el-color-primary-light-8: #ccdefb;
--el-color-primary-light-9: #e5eefd;
--panel-color-primary: #005eeb;
--panel-color-primary-light-8: #196eed;
--panel-color-primary-light-1: #196eed;
--panel-color-primary-light-2: #337eef;
--panel-color-primary-light-3: #4c8ef1;
--panel-color-primary-light-4: #669ef3;
--panel-color-primary-light-5: #7faef5;
--panel-color-primary-light-6: #99bef7;
--panel-color-primary-light-7: #b2cef9;
--panel-color-primary-light-9: #e5eefd;
--el-color-primary: var(--panel-color-primary);
--el-color-primary-light-3: var(--panel-color-primary-light-1);
--el-color-primary-light-5: var(--panel-color-primary-light-5);
--el-color-primary-light-7: var(--panel-color-primary-light-7);
--el-color-primary-light-8: var(--panel-color-primary-light-8);
--el-color-primary-light-9: var(--panel-color-primary-light-9);
--el-text-color-regular: #646a73;
}
html {
--el-box-shadow-light: 0px 0px 4px rgba(0, 94, 235, 0.1) !important;
--el-text-color-regular: #646a73 !important;
// * menu
--el-menu-bg-color: rgba(0, 94, 235, 0.1) !important;
--el-menu-item-bg-color: rgba(255, 255, 255, 0.3);
--el-menu-item-bg-color-active: #ffffff;
--panel-main-bg-color-9: #f4f4f4;
--panel-menu-bg-color: rgba(0, 94, 235, 0.1);
--panel-menu-width: 180px;
--panel-menu-hide-width: 75px;
--panel-text-color: #1f2329;
--panel-border: 1px solid #f2f2f2;
--panel-button-active: #ffffff;
--panel-button-text-color: var(--panel-color-primary);
--panel-button-bg-color: #ffffff;
--panel-footer-border: #e4e7ed;
--panel-terminal-tag-bg-color: #efefef;
--panel-alert-bg: #e2e4ec;
--panel-path-bg: #ffffff;
--panel-footer-bg: #ffffff;
--panel-pie-bg-color: #ffffff;
--el-fill-color-light: #ffffff;
}
.el-notification {

8
frontend/src/utils/theme.ts

@ -0,0 +1,8 @@
export function setPrimaryColor(color: string) {
let setPrimaryColor: (arg0: string) => any;
const xpackModules = import.meta.glob('../xpack/utils/theme/tool.ts', { eager: true });
if (xpackModules['../xpack/utils/theme/tool.ts']) {
setPrimaryColor = xpackModules['../xpack/utils/theme/tool.ts']['setPrimaryColor'] || {};
return setPrimaryColor(color);
}
}

59
frontend/src/utils/theme/tool.ts

@ -1,59 +0,0 @@
import { MsgWarning } from '../message';
/**
* hex颜色转rgb颜色
* @param str 颜色值字符串
* @returns 返回处理后的颜色值
*/
export function hexToRgb(str: any) {
let hexs: any = '';
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(str)) return MsgWarning('输入错误的hex');
str = str.replace('#', '');
hexs = str.match(/../g);
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
return hexs;
}
/**
* rgb颜色转Hex颜色
* @param r 代表红色
* @param g 代表绿色
* @param b 代表蓝色
* @returns 返回处理后的颜色值
*/
export function rgbToHex(r: any, g: any, b: any) {
let reg = /^\d{1,3}$/;
if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return MsgWarning('输入错误的rgb颜色值');
let hexs = [r.toString(16), g.toString(16), b.toString(16)];
for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
return `#${hexs.join('')}`;
}
/**
* 加深颜色值
* @param color 颜色值字符串
* @param level 加深的程度限0-1之间
* @returns 返回处理后的颜色值
*/
export function getDarkColor(color: string, level: number) {
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(color)) return MsgWarning('输入错误的hex颜色值');
let rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
/**
* 变浅颜色值
* @param color 颜色值字符串
* @param level 加深的程度限0-1之间
* @returns 返回处理后的颜色值
*/
export function getLightColor(color: string, level: number) {
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(color)) return MsgWarning('输入错误的hex颜色值');
let rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
return rgbToHex(rgb[0], rgb[1], rgb[2]);
}

22
frontend/src/utils/xpack.ts

@ -9,7 +9,6 @@ export function resetXSetting() {
globalStore.themeConfig.logo = '';
globalStore.themeConfig.logoWithText = '';
globalStore.themeConfig.favicon = '';
globalStore.themeConfig.isGold = false;
}
export function initFavicon() {
@ -18,13 +17,26 @@ export function initFavicon() {
const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement;
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
if (globalStore.isDarkGoldTheme) {
let goldLink = new URL(`../assets/images/favicon-gold.png`, import.meta.url).href;
let goldLink = new URL(`../assets/images/favicon.svg`, import.meta.url).href;
if (globalStore.isProductPro) {
const themeColor = globalStore.themeConfig.primary;
const svg = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="${themeColor}" xmlns="http://www.w3.org/2000/svg">
<path d="M11.1451 18.8875L5.66228 15.7224V8.40336L3.5376 7.1759V16.9488L9.02038 20.114L11.1451 18.8875Z" />
<path d="M18.3397 15.7224L12.0005 19.3819L9.87683 20.6083L12.0005 21.8348L20.4644 16.9488L18.3397 15.7224Z" />
<path d="M12.0015 4.74388L14.1252 3.5174L12.0005 2.28995L3.5376 7.17591L5.66228 8.40337L12.0005 4.74388H12.0015Z" />
<path d="M14.9816 4.01077L12.8569 5.23723L18.3397 8.40336V15.7224L20.4634 16.9488V7.1759L14.9816 4.01077Z" />
<path d="M11.9995 1.02569L21.5576 6.54428V17.5795L11.9995 23.0971L2.44343 17.5795V6.54428L11.9995 1.02569ZM11.9995 0.72728L2.18182 6.39707V17.7366L11.9995 23.4064L21.8182 17.7366V6.39707L11.9995 0.72728Z" />
<path d="M12.3079 6.78001L12.9564 7.16695V17.105L12.3079 17.48V6.78001Z" />
<path d="M12.3078 6.78001L9.10889 8.6222V9.86954H10.2359V16.2854L12.3059 17.481L12.3078 6.78001Z" />
</svg>
`;
goldLink = `data:image/svg+xml,${encodeURIComponent(svg)}`;
link.href = favicon ? '/api/v1/images/favicon' : goldLink;
} else {
link.href = favicon ? '/api/v1/images/favicon' : '/public/favicon.png';
}
document.getElementsByTagName('head')[0].appendChild(link);
document.head.appendChild(link);
}
export async function getXpackSetting() {
@ -102,7 +114,7 @@ export async function getXpackSettingForTheme() {
globalStore.themeConfig.logo = res2.data?.logo;
globalStore.themeConfig.logoWithText = res2.data?.logoWithText;
globalStore.themeConfig.favicon = res2.data?.favicon;
globalStore.themeConfig.isGold = res2.data?.theme === 'dark-gold';
globalStore.themeConfig.themeColor = res2.data?.themeColor;
} else {
resetXSetting();
}

2
frontend/src/views/home/app/index.vue

@ -105,7 +105,7 @@ defineExpose({
.h-app-title {
font-weight: 500;
font-size: 15px;
color: #1f2329;
color: var(--panel-text-color);
}
.h-app-desc {

2
frontend/src/views/host/terminal/terminal/index.vue

@ -3,7 +3,7 @@
<el-tabs
type="card"
class="terminal-tabs"
style="background-color: #efefef; margin-top: 20px"
style="background-color: var(--panel-terminal-tag-bg-color); margin-top: 20px"
v-model="terminalValue"
:before-leave="beforeLeave"
@tab-change="quickCmd = ''"

31
frontend/src/views/login/components/login-form.vue

@ -476,7 +476,11 @@ onMounted(() => {
height: 45px;
margin-top: 10px;
background-color: #005eeb;
border-color: #005eeb;
color: #ffffff;
&:hover {
--el-button-hover-border-color: #005eeb;
}
}
.demo {
@ -503,6 +507,13 @@ onMounted(() => {
color: #005eeb;
}
:deep(a) {
color: #005eeb;
&:hover {
color: #005eeb95;
}
}
:deep(.el-checkbox__input .el-checkbox__inner) {
background-color: #fff !important;
border-color: #fff !important;
@ -523,4 +534,24 @@ onMounted(() => {
margin-left: 20px;
}
}
.cursor-pointer {
outline: none;
}
.el-dropdown:focus-visible {
outline: none;
}
.el-tooltip__trigger:focus-visible {
outline: none;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
color: #005eeb !important;
background-color: #e5eefd !important;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
color: #005eeb !important;
background-color: #e5eefd !important;
}
</style>

8
frontend/src/views/setting/license/index.vue

@ -2,7 +2,7 @@
<div>
<LayoutContent v-loading="loading" :title="$t('setting.license')" :divider="true">
<template #main>
<el-row :gutter="20" class="mt-5; mb-10">
<el-row :gutter="20" class="mt-5; mb-10 license-card">
<el-col :xs="24" :sm="24" :md="15" :lg="15" :xl="15">
<div class="descriptions" v-if="hasLicense">
<el-descriptions :column="1" direction="horizontal" size="large" border>
@ -123,6 +123,7 @@ import LicenseImport from '@/components/license-import/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
import { initFavicon } from '@/utils/xpack';
const loading = ref();
const licenseRef = ref();
const globalStore = GlobalStore();
@ -168,7 +169,7 @@ const onUnBind = async () => {
.then(() => {
loading.value = false;
globalStore.isProductPro = false;
globalStore.themeConfig.isGold = false;
initFavicon();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
window.location.reload();
})
@ -251,6 +252,9 @@ onMounted(() => {
background-color: rgba(0, 94, 235, 0.03);
}
}
:deep(.license-card .el-card) {
border: var(--panel-border) !important;
}
:deep(.el-descriptions__content) {
max-width: 300px;
}

88
frontend/src/views/setting/panel/index.vue

@ -27,20 +27,27 @@
</el-form-item>
<el-form-item :label="$t('setting.theme')" prop="theme">
<el-radio-group @change="onSave('Theme', form.theme)" v-model="form.theme">
<el-radio-button v-if="isProductPro" value="dark-gold">
<span>{{ $t('setting.darkGold') }}</span>
</el-radio-button>
<el-radio-button value="light">
<span>{{ $t('setting.light') }}</span>
</el-radio-button>
<el-radio-button value="dark">
<span>{{ $t('setting.dark') }}</span>
</el-radio-button>
<el-radio-button value="auto">
<span>{{ $t('setting.auto') }}</span>
</el-radio-button>
</el-radio-group>
<div class="flex justify-between items-center gap-6">
<el-radio-group @change="onSave('Theme', form.theme)" v-model="form.theme">
<el-radio-button value="light">
<span>{{ $t('setting.light') }}</span>
</el-radio-button>
<el-radio-button value="dark">
<span>{{ $t('setting.dark') }}</span>
</el-radio-button>
<el-radio-button value="auto">
<span>{{ $t('setting.auto') }}</span>
</el-radio-button>
</el-radio-group>
<el-button
v-if="isProductPro"
@click="onChangeThemeColor"
icon="Setting"
class="!h-[34px]"
>
<span>{{ $t('container.custom') }}</span>
</el-button>
</div>
</el-form-item>
<el-form-item :label="$t('setting.menuTabs')" prop="menuTabs">
@ -164,6 +171,7 @@
<Timeout ref="timeoutRef" @search="search()" />
<Network ref="networkRef" @search="search()" />
<HideMenu ref="hideMenuRef" @search="search()" />
<ThemeColor ref="themeColorRef" />
</div>
</template>
@ -183,8 +191,10 @@ import SystemIP from '@/views/setting/panel/systemip/index.vue';
import Proxy from '@/views/setting/panel/proxy/index.vue';
import Network from '@/views/setting/panel/default-network/index.vue';
import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
import ThemeColor from '@/views/setting/panel/theme-color/index.vue';
import { storeToRefs } from 'pinia';
import { getXpackSetting, updateXpackSettingByKey } from '@/utils/xpack';
import { setPrimaryColor } from '@/utils/theme';
const loading = ref(false);
const i18n = useI18n();
@ -198,6 +208,11 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
interface ThemeColor {
light: string;
dark: string;
}
const form = reactive({
userName: '',
password: '',
@ -209,6 +224,7 @@ const form = reactive({
panelName: '',
systemIP: '',
theme: '',
themeColor: {} as ThemeColor,
menuTabs: '',
language: '',
complexityVerification: '',
@ -238,6 +254,7 @@ const proxyRef = ref();
const timeoutRef = ref();
const networkRef = ref();
const hideMenuRef = ref();
const themeColorRef = ref();
const unset = ref(i18n.t('setting.unSetting'));
interface Node {
@ -279,15 +296,16 @@ const search = async () => {
const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedTitles = getCheckedTitles(json);
form.proHideMenus = checkedTitles.toString();
if (isProductPro.value) {
const xpackRes = await getXpackSetting();
if (xpackRes) {
form.theme = xpackRes.data.theme === 'dark-gold' ? 'dark-gold' : res.data.theme;
return;
form.theme = xpackRes.data.theme || globalStore.themeConfig.theme;
form.themeColor = JSON.parse(xpackRes.data.themeColor);
globalStore.themeConfig.themeColor = xpackRes.data.themeColor;
}
} else {
form.theme = res.data.theme;
}
form.theme = res.data.theme;
};
function extractTitles(node: Node, result: string[]): void {
@ -347,6 +365,11 @@ const onChangeHideMenus = () => {
hideMenuRef.value.acceptParams({ menuList: form.hideMenuList });
};
const onChangeThemeColor = () => {
const themeColor: ThemeColor = JSON.parse(globalStore.themeConfig.themeColor);
themeColorRef.value.acceptParams({ themeColor: themeColor, theme: globalStore.themeConfig.theme });
};
const onSave = async (key: string, val: any) => {
loading.value = true;
if (key === 'Language') {
@ -354,21 +377,24 @@ const onSave = async (key: string, val: any) => {
globalStore.updateLanguage(val);
}
if (key === 'Theme') {
if (val === 'dark-gold') {
globalStore.themeConfig.isGold = true;
} else {
globalStore.themeConfig.isGold = false;
globalStore.themeConfig.theme = val;
}
globalStore.themeConfig.theme = val;
switchTheme();
if (globalStore.isProductPro) {
updateXpackSettingByKey('Theme', val === 'dark-gold' ? 'dark-gold' : '');
if (val === 'dark-gold') {
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
loading.value = false;
search();
return;
await updateXpackSettingByKey('Theme', val);
let color: string;
const themeColor: ThemeColor = JSON.parse(globalStore.themeConfig.themeColor);
if (val === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
color = prefersDark.matches ? themeColor.dark : themeColor.light;
} else {
color = val === 'dark' ? themeColor.dark : themeColor.light;
}
globalStore.themeConfig.primary = color;
setPrimaryColor(color);
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
loading.value = false;
await search();
return;
}
}
if (key === 'MenuTabs') {
@ -385,7 +411,7 @@ const onSave = async (key: string, val: any) => {
}
loading.value = false;
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
search();
await search();
})
.catch(() => {
loading.value = false;

226
frontend/src/views/setting/panel/theme-color/index.vue

@ -0,0 +1,226 @@
<template>
<div>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('xpack.theme.customColor')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.light')" prop="light">
<div class="flex flex-wrap justify-between items-center sm:items-start">
<div class="flex gap-1">
<el-tooltip :content="$t('xpack.theme.classicBlue')" placement="top">
<el-button
color="#005eeb"
circle
dark
:class="form.light === '#005eeb' ? 'selected-white' : ''"
@click="changeLightColor('#005eeb')"
/>
</el-tooltip>
<el-tooltip :content="$t('xpack.theme.freshGreen')" placement="top">
<el-button
color="#00CC00"
:class="form.light === '#00CC00' ? 'selected-white' : ''"
circle
dark
@click="changeLightColor('#00CC00')"
/>
</el-tooltip>
<el-color-picker
v-model="form.light"
class="ml-4"
:predefine="predefineColors"
show-alpha
/>
</div>
</div>
</el-form-item>
<el-form-item :label="$t('setting.dark')" prop="dark">
<div class="flex flex-wrap justify-between items-center sm:items-start">
<div class="flex flex-wrap justify-between items-center mt-4 sm:mt-0">
<div class="flex gap-1">
<el-tooltip :content="$t('xpack.theme.classicBlue')" placement="top">
<el-button
color="#3D8EFF"
circle
dark
:class="form.dark === '#3D8EFF' ? 'selected-white' : ''"
@click="changeDarkColor('#3D8EFF')"
/>
</el-tooltip>
<el-tooltip :content="$t('xpack.theme.lingXiaGold')" placement="top">
<el-button
color="#F0BE96"
:class="form.dark === '#F0BE96' ? 'selected-white' : ''"
circle
dark
@click="changeDarkColor('#F0BE96')"
/>
</el-tooltip>
<el-tooltip :content="$t('xpack.theme.freshGreen')" placement="top">
<el-button
color="#00CC00"
:class="form.dark === '#00CC00' ? 'selected-white' : ''"
circle
dark
@click="changeDarkColor('#00CC00')"
/>
</el-tooltip>
<el-color-picker
v-model="form.dark"
class="ml-4"
:predefine="predefineColors"
show-alpha
/>
</div>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onReSet">{{ $t('xpack.theme.setDefault') }}</el-button>
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { FormInstance } from 'element-plus';
import { initFavicon, updateXpackSettingByKey } from '@/utils/xpack';
import { setPrimaryColor } from '@/utils/theme';
import { GlobalStore } from '@/store';
const emit = defineEmits<{ (e: 'search'): void }>();
const drawerVisible = ref();
const loading = ref();
interface DialogProps {
themeColor: { light: string; dark: string };
theme: '';
}
interface ThemeColor {
light: string;
dark: string;
}
const form = reactive({
themeColor: {} as ThemeColor,
theme: '',
light: '',
dark: '',
});
const formRef = ref<FormInstance>();
const predefineColors = ref([
'#005eeb',
'#3D8EFF',
'#F0BE96',
'#00CC00',
'#00ced1',
'#c71585',
'#ff4500',
'#ff8c00',
'#ffd700',
]);
const globalStore = GlobalStore();
const acceptParams = (params: DialogProps): void => {
form.themeColor = params.themeColor;
form.theme = params.theme;
form.dark = form.themeColor.dark;
form.light = form.themeColor.light;
drawerVisible.value = true;
};
const changeLightColor = (color: string) => {
form.light = color;
};
const changeDarkColor = (color: string) => {
form.dark = color;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid) => {
if (!valid) return;
form.themeColor = { light: form.light, dark: form.dark };
if (globalStore.isProductPro) {
await updateXpackSettingByKey('ThemeColor', JSON.stringify(form.themeColor));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
globalStore.themeConfig.themeColor = JSON.stringify(form.themeColor);
loading.value = false;
let color: string;
if (form.theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
color = prefersDark.matches ? form.dark : form.light;
} else {
color = form.theme === 'dark' ? form.dark : form.light;
}
globalStore.themeConfig.primary = color;
setPrimaryColor(color);
initFavicon();
drawerVisible.value = false;
emit('search');
return;
}
});
};
const onReSet = async () => {
form.themeColor = { light: '#005eeb', dark: '#F0BE96' };
if (globalStore.isProductPro) {
await updateXpackSettingByKey('ThemeColor', JSON.stringify(form.themeColor));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
globalStore.themeConfig.themeColor = JSON.stringify(form.themeColor);
let color: string;
if (form.theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
color = prefersDark.matches ? '#F0BE96' : '#005eeb';
} else {
color = form.theme === 'dark' ? '#F0BE96' : '#005eeb';
}
globalStore.themeConfig.primary = color;
setPrimaryColor(color);
initFavicon();
drawerVisible.value = false;
return;
}
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.selected-white {
box-shadow: inset 0 0 0 1px white;
}
</style>
Loading…
Cancel
Save