mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-27 12:39:01 +08:00
feat: 增加自定义主题颜色,调整默认暗色主题配色 (#6964)
This commit is contained in:
parent
8d35c54672
commit
ffdd9b9d79
1
frontend/src/assets/images/menu-bg.svg
Normal file
1
frontend/src/assets/images/menu-bg.svg
Normal file
@ -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 |
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ const goHome = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 55px;
|
||||
z-index: 1;
|
||||
img {
|
||||
object-fit: contain;
|
||||
width: 95%;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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-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-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;
|
||||
--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;
|
||||
|
||||
.el-tag.el-tag--info {
|
||||
--el-tag-bg-color: rgb(49, 51, 51);
|
||||
--el-tag-border-color: rgb(64, 67, 67);
|
||||
}
|
||||
.el-tag.el-tag--light {
|
||||
--el-tag-bg-color: #111417;
|
||||
--el-tag-border-color: var(--el-color-primary);
|
||||
}
|
||||
.el-tag.el-tag--success {
|
||||
--el-tag-border-color: var(--el-color-success);
|
||||
}
|
||||
.el-tag.el-tag--danger {
|
||||
--el-tag-border-color: var(--el-color-danger);
|
||||
}
|
||||
.el-card {
|
||||
--el-card-bg-color: rgb(35, 35, 35);
|
||||
color: #ffffff;
|
||||
border: 1px solid var(--el-card-border-color) !important;
|
||||
--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-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-message-box {
|
||||
--el-messagebox-title-color: var(--el-menu-text-color);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
.el-menu-item:hover, .el-sub-menu__title:hover{
|
||||
background: var(--panel-main-bg-color-8) !important;
|
||||
}
|
||||
|
||||
.el-alert--info {
|
||||
--el-alert-bg-color: rgb(56, 59, 59);
|
||||
.el-menu .el-menu-item {
|
||||
box-shadow: 0 0 4px rgba(36, 38, 51, .72);
|
||||
}
|
||||
|
||||
.el-menu .el-sub-menu__title {
|
||||
box-shadow: 0 0 4px rgba(36, 38, 51, .72);
|
||||
}
|
||||
|
||||
.el-overlay {
|
||||
background-color: #2E313D90;
|
||||
}
|
||||
|
||||
.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-tabs--card > .el-tabs__header .el-tabs__nav {
|
||||
border: 1px solid var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--el-border-color-darker);
|
||||
}
|
||||
// sidebar
|
||||
.sidebar-container-popper {
|
||||
border: 1px solid #66686c;
|
||||
.el-menu--popup-container {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.sidebar-container {
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-collapse {
|
||||
color: var(--el-menu-text-color);
|
||||
border: var(--panel-border);
|
||||
.el-card {
|
||||
--el-card-bg-color: var(--panel-main-bg-color-10)
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
.system-label {
|
||||
color: var(--el-menu-text-color);
|
||||
.el-button--primary {
|
||||
--el-button-hover-link-text-color: var(--panel-color-primary-light-1);
|
||||
}
|
||||
|
||||
.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-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);
|
||||
}
|
||||
// content-box
|
||||
.content-box {
|
||||
.text {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
}
|
||||
|
||||
.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-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);
|
||||
}
|
||||
|
||||
.el-button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.el-button.is-disabled:hover {
|
||||
border-color: var(--panel-main-bg-color-8);
|
||||
background: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
.el-button--primary.is-link.is-disabled {
|
||||
color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.el-alert--error {
|
||||
--el-alert-bg-color: var(--panel-alert-error-bg-color);
|
||||
--el-color-error: var(--panel-alert-error-text-color);
|
||||
}
|
||||
|
||||
.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);
|
||||
.el-alert--success {
|
||||
--el-alert-bg-color: var(--panel-alert-success-bg-color);
|
||||
--el-color-success: var(--panel-alert-success-text-color);;
|
||||
}
|
||||
|
||||
.h-app-card {
|
||||
.h-app-content {
|
||||
.h-app-title {
|
||||
color: #f2f8ff;
|
||||
}
|
||||
}
|
||||
.el-alert--warning {
|
||||
--el-alert-bg-color: var(--panel-alert-warning-bg-color);
|
||||
--el-color-warning: var(--panel-alert-warning-text-color);
|
||||
}
|
||||
|
||||
.infinite-list .infinite-list-item {
|
||||
background: #212426;
|
||||
&:hover {
|
||||
background: #212426;
|
||||
}
|
||||
.el-alert--info {
|
||||
--el-alert-bg-color: var(--panel-alert-info-bg-color);
|
||||
--el-color-info: var(--panel-alert-info-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-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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Normal file
8
frontend/src/utils/theme.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ defineExpose({
|
||||
.h-app-title {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
color: #1f2329;
|
||||
color: var(--panel-text-color);
|
||||
}
|
||||
|
||||
.h-app-desc {
|
||||
|
@ -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 = ''"
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
Normal file
226
frontend/src/views/setting/panel/theme-color/index.vue
Normal file
@ -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…
Reference in New Issue
Block a user