Browse Source

refactor: refactor theme and logo switching functionality (#5015)

pull/5105/head
Takagi 6 months ago committed by GitHub
parent
commit
63c330054e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      frontend/package.json
  2. BIN
      frontend/src/assets/images/1panel-logo-dark.png
  3. BIN
      frontend/src/assets/images/1panel-logo-light.png
  4. 1
      frontend/src/assets/images/1panel-logo.svg
  5. BIN
      frontend/src/assets/images/1panel-menu-dark.png
  6. BIN
      frontend/src/assets/images/1panel-menu-light.png
  7. 8
      frontend/src/assets/images/1panel-menu-logo.svg
  8. 4
      frontend/src/components/license-import/index.vue
  9. 9
      frontend/src/components/system-upgrade/index.vue
  10. 9
      frontend/src/components/v-charts/components/Line.vue
  11. 18
      frontend/src/components/v-charts/components/Pie.vue
  12. 16
      frontend/src/hooks/use-logo.ts
  13. 74
      frontend/src/hooks/use-theme.ts
  14. 35
      frontend/src/layout/components/Sidebar/components/Logo.vue
  15. 58
      frontend/src/layout/index.vue
  16. 4
      frontend/src/main.ts
  17. 5
      frontend/src/store/modules/global.ts
  18. 25
      frontend/src/styles/element-dark.scss
  19. 18
      frontend/src/styles/element.scss
  20. 2
      frontend/src/styles/var.scss
  21. 17
      frontend/src/utils/xpack.ts
  22. 9
      frontend/src/views/app-store/detail/index.vue
  23. 30
      frontend/src/views/login/entrance/index.vue
  24. 30
      frontend/src/views/login/index.vue
  25. 12
      frontend/src/views/setting/about/index.vue
  26. 39
      frontend/src/views/setting/panel/index.vue
  27. 2
      frontend/vite-env.d.ts
  28. 5
      frontend/vite.config.ts

1
frontend/package.json

@ -87,6 +87,7 @@
"vite-plugin-eslint": "^1.6.0", "vite-plugin-eslint": "^1.6.0",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"vite-plugin-vue-setup-extend": "^0.4.0", "vite-plugin-vue-setup-extend": "^0.4.0",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^0.29.8" "vue-tsc": "^0.29.8"
}, },
"overrides": { "overrides": {

BIN
frontend/src/assets/images/1panel-logo-dark.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

BIN
frontend/src/assets/images/1panel-logo-light.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

1
frontend/src/assets/images/1panel-logo.svg

@ -0,0 +1 @@
<svg fill="currentColor" height="33" viewBox="0 0 120 33" width="120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h120v32.7791h-120z"/></clipPath><g clip-path="url(#a)"><path d="m12.9834 26.1691-7.8556-4.5363v-10.4865l-3.04418-1.75723v14.00093l7.85558 4.5364z"/><path d="m23.2918 21.6342-9.0827 5.2418-3.0442 1.7572 3.0442 1.7573 12.1268-6.9991z"/><path d="m14.209 5.90309 3.0442-1.75724-3.0442-1.75724-12.12538 7.00048 3.04418 1.75721z"/><path d="m18.4803 4.85416-3.0441 1.75725 7.8555 4.53489v10.4865l3.0428 1.7572v-14.00092z"/><path d="m14.2091.577186 13.6931 7.905464v15.81945l-13.6931 7.9055-13.693125-7.9126v-15.81947l13.693125-7.905459m0-.427554-14.066522 8.123513v16.24707l14.066522 8.1235 14.0679-8.1235v-16.24707z"/><path d="m14.6494 8.82043.9292.55438v14.24039l-.9292.5359z"/><path d="m14.6494 8.82043-4.5834 2.64087v1.7872h1.6133v9.1909l2.9687 1.7131z"/><path d="m65.078 17.447c-.971 2.247-3.0613 3.371-6.2708 3.372-.409.0271-3.1354-.0841-3.7525-.1425v6.5829h-5.2047v-20.86321s5.1306-.32209 8.9601-.14252c3.2437.1582 4.8983.91924 5.9145 2.76057 1.0289 1.90406 1.3225 6.18386.3534 8.43276zm-4.7587-6.6983c-.3306-.3492-.8551-.5573-1.5677-.6385-.7895-.095-2.0223-.0679-3.6983.0812v6.6741c1.281.1611 2.5759.1788 3.8608.0528.5886-.0556 1.0105-.2309 1.2641-.5316.7297-.5915 1.2699-4.4494.1411-5.6395z"/><path d="m44.8703 27.1069h-4.8071v-16.2314h-3.4062l-.2851-4.27549h8.4998z"/><path d="m116.041 21.4447c0 .9805.321 1.5828.965 1.8071.329.0883 1.341.1425 1.948.1739.107.0085.214.0128.322.0128h.634v3.7881c-.276.0181-.642.0271-1.097.0271-3.187 0-5.923-.285-6.905-2.5297-.26-.6599-.305-2.1378-.358-2.7306-.021-.237-.03-.4748-.027-.7126v-15.35776l4.524-.55582z"/><path d="m109.783 18.5173v2.6252h-8.285v.4475c0 1.0015.307 1.6105.921 1.8271.53.1419 1.076.21 1.625.2024h.365c1.624 0 2.609-.0442 4.314-.3962h.027l.681 3.5786c0 .0437-.776.1924-2.329.4461-1.115.1153-2.235.1696-3.356.1625-3.25 0-5.3478-1.0965-6.2951-3.2893-.3518-.8638-.5314-1.7881-.5288-2.7207v-3.5473c0-2.5995 1.0927-4.3278 3.2779-5.1848 1.002-.3209 2.048-.4806 3.1-.4731 3.059 0 5.044.9976 5.956 2.9928.351.7848.527 1.8946.527 3.3292zm-4.576-.6912c0-1.0289-.221-1.638-.663-1.827-.362-.234-.787-.3523-1.219-.3392-1.218 0-1.827.5734-1.827 1.7202v.6954h3.706z"/><path d="m79.7886 18.1112v9.1211h-3.9905l-.0185-.9235c-1.3948.7259-2.7577 1.0884-4.0888 1.0874h-.3934c-1.3682 0-2.4551-.5254-3.2608-1.5763-.533-.8196-.8116-1.7786-.801-2.7562v-.1098c0-1.7929.6295-2.9833 1.8884-3.5715.5159-.3449 1.8242-.5168 3.925-.5159h2.1463v-.5031c0-1.0318-.1497-1.6247-.449-1.7786-.323-.2413-.8978-.362-1.7244-.362h-4.2756l.0727-3.6556 4.9254-.1339c2.9245 0 4.7354.7244 5.4328 2.1734.4076.8427.6114 2.0109.6114 3.5045zm-7.6803 3.7909c-.1354.1611-.285.687-.285 1.0575 0 .8152.4166 1.2228 1.2499 1.2228.4978 0 1.2043-.2038 2.1192-.6114v-1.9952s-2.5881-.2637-3.0841.3263z"/><path d="m94.867 18.5658v8.6936h-4.6005v-9.5173c0-.925-.3093-1.3625-1.3055-1.6732s-2.5154.067-2.8418.2665v10.924h-4.5634v-14.8846h4.2214s-.0556.8551-.02.9791c.0606-.0233.1196-.0505.1767-.0812l.2722-.1425c.2743-.1338.556-.2518.8437-.3535.3937-.1525.7981-.2754 1.21-.3677.5055-.1086 1.0196-.172 1.5364-.1895 1.0451-.0456 1.8788.0446 2.5012.2708.8171.3097 1.4878.8993 2.0123 1.7686.3725.6869.5582 2.1226.5573 4.3069z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
frontend/src/assets/images/1panel-menu-dark.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

BIN
frontend/src/assets/images/1panel-menu-light.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

8
frontend/src/assets/images/1panel-menu-logo.svg

@ -0,0 +1,8 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M19.4207 4.84191L7.78421 11.5601V21.6237L15.323 25.9772L12.4016 27.6635L4.86277 23.3101V9.87373L16.4993 3.15553L19.4207 4.84191Z"/>
<path d="M25.2158 21.6251L13.5779 28.3419L16.4993 30.0283L28.1372 23.3115L25.2158 21.6251Z"/>
<path d="M20.5983 5.52166L17.6769 7.20806L25.2157 11.5601V21.6237L28.1358 23.3101V9.87372L20.5983 5.52166Z"/>
<path d="M16.9219 9.32801L17.8136 9.86003V23.5262L16.9219 24.0405V9.32801Z"/>
<path d="M12.5233 11.8624L16.9219 9.32801L16.9205 24.0418L14.0716 22.3979V13.5775H12.5233V11.8624Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 8.79596L16.4993 1L30 8.79596V24.3879L16.4993 32.1838L3 24.3879V8.79596ZM29.6403 9.00384L16.4993 1.41714V1.41031L3.35834 8.99701V24.1786L16.4993 31.7722L29.6403 24.1854V9.00384Z" />
</svg>

After

Width:  |  Height:  |  Size: 869 B

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

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<el-dialog v-model="open" :close-on-click-modal="false" @close="handleClose"> <el-dialog class="level-up-pro" v-model="open" :close-on-click-modal="false" @close="handleClose">
<div style="text-align: center" v-loading="loading"> <div style="text-align: center" v-loading="loading">
<span class="text-3xl font-medium">{{ $t('license.levelUpPro') }}</span> <span class="text-3xl font-medium title">{{ $t('license.levelUpPro') }}</span>
<el-row type="flex" justify="center" class="mt-6"> <el-row type="flex" justify="center" class="mt-6">
<el-col :span="22"> <el-col :span="22">
<el-upload <el-upload

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

@ -64,11 +64,7 @@
{{ upgradeInfo.testVersion }} {{ upgradeInfo.testVersion }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
<MdEditor <MdEditor v-model="upgradeInfo.releaseNote" previewOnly :theme="isDarkTheme ? 'dark' : 'light'" />
v-model="upgradeInfo.releaseNote"
previewOnly
:theme="globalStore.$state.themeConfig.theme === 'dark' ? 'dark' : 'light'"
/>
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@ -88,7 +84,10 @@ import { MsgSuccess } from '@/utils/message';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const version = ref<string>(''); const version = ref<string>('');
const isProductPro = ref(); const isProductPro = ref();

9
frontend/src/components/v-charts/components/Line.vue

@ -6,7 +6,10 @@ import { onMounted, nextTick, watch, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { computeSizeFromKBs, computeSizeFromKB, computeSizeFromMB } from '@/utils/util'; import { computeSizeFromKBs, computeSizeFromKB, computeSizeFromMB } from '@/utils/util';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const props = defineProps({ const props = defineProps({
id: { id: {
type: String, type: String,
@ -89,8 +92,6 @@ function initChart() {
itemChart = echarts.init(document.getElementById(props.id) as HTMLElement); itemChart = echarts.init(document.getElementById(props.id) as HTMLElement);
} }
const theme = globalStore.$state.themeConfig.theme || 'light';
const series = []; const series = [];
if (props.option?.yData?.length) { if (props.option?.yData?.length) {
props.option?.yData.forEach((item: any, index: number) => { props.option?.yData.forEach((item: any, index: number) => {
@ -112,7 +113,7 @@ function initChart() {
show: true, show: true,
lineStyle: { lineStyle: {
type: 'dashed', type: 'dashed',
opacity: theme === 'dark' ? 0.1 : 1, opacity: isDarkTheme.value ? 0.1 : 1,
}, },
}, },
...item, ...item,
@ -184,7 +185,7 @@ function initChart() {
//线 //线
lineStyle: { lineStyle: {
type: 'dashed', //线 线0 type: 'dashed', //线 线0
opacity: theme === 'dark' ? 0.1 : 1, // opacity: isDarkTheme.value ? 0.1 : 1, //
}, },
}, },
}, },

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

@ -5,7 +5,10 @@
import { onMounted, nextTick, watch, onBeforeUnmount } from 'vue'; import { onMounted, nextTick, watch, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { isDarkGoldTheme, isDarkTheme } = storeToRefs(globalStore);
const props = defineProps({ const props = defineProps({
id: { id: {
type: String, type: String,
@ -30,7 +33,6 @@ function initChart() {
if (myChart === null || myChart === undefined) { if (myChart === null || myChart === undefined) {
myChart = echarts.init(document.getElementById(props.id) as HTMLElement); myChart = echarts.init(document.getElementById(props.id) as HTMLElement);
} }
const theme = globalStore.$state.themeConfig.theme || 'light';
let percentText = String(props.option.data).split('.'); let percentText = String(props.option.data).split('.');
const option = { const option = {
title: [ title: [
@ -47,7 +49,7 @@ function initChart() {
}, },
}, },
color: theme === 'dark' ? '#ffffff' : '#0f0f0f', color: isDarkTheme.value ? '#ffffff' : '#0f0f0f',
lineHeight: 25, lineHeight: 25,
// fontSize: 20, // fontSize: 20,
fontWeight: 500, fontWeight: 500,
@ -56,7 +58,7 @@ function initChart() {
top: '32%', top: '32%',
subtext: props.option.title, subtext: props.option.title,
subtextStyle: { subtextStyle: {
color: theme === 'dark' ? '#BBBFC4' : '#646A73', color: isDarkTheme.value ? '#BBBFC4' : '#646A73',
fontSize: 13, fontSize: 13,
}, },
textAlign: 'center', textAlign: 'center',
@ -91,17 +93,17 @@ function initChart() {
showBackground: true, showBackground: true,
coordinateSystem: 'polar', coordinateSystem: 'polar',
backgroundStyle: { backgroundStyle: {
color: theme === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)', color: isDarkTheme.value ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)',
}, },
color: [ color: [
new echarts.graphic.LinearGradient(0, 1, 0, 0, [ new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ {
offset: 0, offset: 0,
color: 'rgba(81, 192, 255, .1)', color: isDarkGoldTheme.value ? '#836c4c' : 'rgba(81, 192, 255, .1)',
}, },
{ {
offset: 1, offset: 1,
color: '#4261F6', color: isDarkGoldTheme.value ? '#eaba63' : '#4261F6',
}, },
]), ]),
], ],
@ -117,12 +119,12 @@ function initChart() {
label: { label: {
show: false, show: false,
}, },
color: theme === 'dark' ? '#16191D' : '#fff', color: isDarkTheme.value ? '#16191D' : '#fff',
data: [ data: [
{ {
value: 0, value: 0,
itemStyle: { itemStyle: {
shadowColor: theme === 'dark' ? '#16191D' : 'rgba(0, 94, 235, 0.1)', shadowColor: isDarkTheme.value ? '#16191D' : 'rgba(0, 94, 235, 0.1)',
shadowBlur: 5, shadowBlur: 5,
}, },
}, },

16
frontend/src/hooks/use-logo.ts

@ -1,14 +1,16 @@
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { searchXSetting } from '@/xpack/api/modules/setting'; import { getXpackSetting } from '@/utils/xpack';
export const useLogo = async () => { export const useLogo = async () => {
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const res = await searchXSetting(); const res = await getXpackSetting();
localStorage.setItem('1p-favicon', res.data.logo); if (res) {
globalStore.themeConfig.title = res.data.title; localStorage.setItem('1p-favicon', res.data.logo);
globalStore.themeConfig.logo = res.data.logo; globalStore.themeConfig.title = res.data.title;
globalStore.themeConfig.logoWithText = res.data.logoWithText; globalStore.themeConfig.logo = res.data.logo;
globalStore.themeConfig.favicon = res.data.favicon; globalStore.themeConfig.logoWithText = res.data.logoWithText;
globalStore.themeConfig.favicon = res.data.favicon;
}
const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement; const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement;
link.type = 'image/x-icon'; link.type = 'image/x-icon';

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

@ -1,48 +1,62 @@
import { computed, onBeforeMount } from 'vue'; import { onBeforeMount, watch } from 'vue';
import { getLightColor, getDarkColor } from '@/utils/theme/tool';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { MsgSuccess } from '@/utils/message'; import { storeToRefs } from 'pinia';
export const useTheme = () => { export const useTheme = () => {
const globalStore = GlobalStore(); const { themeConfig } = storeToRefs(GlobalStore());
const themeConfig = computed(() => globalStore.themeConfig); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
const switchDark = () => { /**
* This method is only executed when loading or manually switching for the first time.
*/
const switchTheme = () => {
if (themeConfig.value.theme === 'auto') { if (themeConfig.value.theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
themeConfig.value.theme = prefersDark.matches ? 'dark' : 'light'; themeConfig.value.theme = prefersDark.matches ? 'dark' : 'light';
if (prefersDark.addEventListener) {
prefersDark.addEventListener('change', switchAccordingUserProxyTheme);
} else if (prefersDark.addListener) {
prefersDark.addListener(switchAccordingUserProxyTheme);
}
} else {
prefersDark.removeEventListener('change', switchAccordingUserProxyTheme);
prefersDark.removeListener(switchAccordingUserProxyTheme);
} }
updateTheme(themeConfig.value.theme);
};
const switchAccordingUserProxyTheme = (event: MediaQueryListEvent) => {
const preferTheme = event.matches ? 'dark' : 'light';
themeConfig.value.theme = preferTheme;
updateTheme(preferTheme);
};
const updateTheme = (theme: string) => {
const body = document.documentElement as HTMLElement; const body = document.documentElement as HTMLElement;
if (themeConfig.value.theme === 'dark') body.setAttribute('class', 'dark'); body.setAttribute('class', theme);
else body.setAttribute('class', '');
}; };
const changePrimary = (val: string) => { onBeforeMount(() => {
if (!val) { updateTheme(themeConfig.value.theme);
val = '#409EFF'; });
MsgSuccess('主题颜色已重置为 #409EFF');
} /**
globalStore.setThemeConfig({ ...themeConfig.value, primary: val }); * Called internally by the system for automatically switching themes
document.documentElement.style.setProperty( */
'--el-color-primary-dark-2', const autoSwitchTheme = () => {
`${getDarkColor(themeConfig.value.primary, 0.1)}`, let preferTheme = themeConfig.value.theme;
); if (themeConfig.value.theme === 'auto') {
document.documentElement.style.setProperty('--el-color-primary', themeConfig.value.primary); preferTheme = prefersDark.matches ? 'dark' : 'light';
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
`${getLightColor(themeConfig.value.primary, i / 10)}`,
);
} }
updateTheme(preferTheme);
}; };
onBeforeMount(() => { watch(themeConfig, () => {
switchDark(); autoSwitchTheme();
changePrimary(themeConfig.value.primary);
}); });
return { return {
switchDark, autoSwitchTheme,
changePrimary, switchTheme,
}; };
}; };

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

@ -1,35 +1,34 @@
<template> <template>
<div class="logo"> <div class="logo" style="cursor: pointer" @click="goHome">
<img :src="getLogoUrl(isCollapse)" style="cursor: pointer" alt="logo" @click="goHome" /> <template v-if="isCollapse">
<img v-if="globalStore.themeConfig.logo" :src="'/api/v1/images/logo'" style="cursor: pointer" alt="logo" />
<MenuLogo v-else />
</template>
<template v-else>
<img
v-if="globalStore.themeConfig.logoWithText"
:src="'/api/v1/images/logoWithText'"
style="cursor: pointer"
alt="logo"
/>
<PrimaryLogo v-else />
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import router from '@/routers'; import router from '@/routers';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import PrimaryLogo from '@/assets/images/1panel-logo.svg?component';
import MenuLogo from '@/assets/images/1panel-menu-logo.svg?component';
defineProps<{ isCollapse: boolean }>(); defineProps<{ isCollapse: boolean }>();
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const goHome = () => { const goHome = () => {
router.push({ name: 'home' }); router.push({ name: 'home' });
}; };
const getLogoUrl = (isCollapse: boolean) => {
if (isCollapse) {
if (globalStore.themeConfig.logo) {
return '/api/v1/images/logo';
} else {
return new URL(`../../../../assets/images/1panel-logo-light.png`, import.meta.url).href;
}
} else {
if (globalStore.themeConfig.logoWithText) {
return '/api/v1/images/logoWithText';
} else {
return new URL(`../../../../assets/images/1panel-menu-light.png`, import.meta.url).href;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

58
frontend/src/layout/index.vue

@ -24,7 +24,7 @@ import { useI18n } from 'vue-i18n';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import { getLicenseStatus, getSettingInfo, getSystemAvailable } from '@/api/modules/setting'; import { getLicenseStatus, getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { initFavicon, resetXSetting } from '@/utils/xpack'; import { getXpackSetting, initFavicon, resetXSetting } from '@/utils/xpack';
useResize(); useResize();
const router = useRouter(); const router = useRouter();
@ -37,7 +37,7 @@ const i18n = useI18n();
const loading = ref(false); const loading = ref(false);
const loadingText = ref(); const loadingText = ref();
const themeConfig = computed(() => globalStore.themeConfig); const themeConfig = computed(() => globalStore.themeConfig);
const { switchDark } = useTheme(); const { switchTheme } = useTheme();
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
@ -79,26 +79,20 @@ const loadDataFromDB = async () => {
globalStore.entrance = res.data.securityEntrance; globalStore.entrance = res.data.securityEntrance;
globalStore.setOpenMenuTabs(res.data.menuTabs === 'enable'); globalStore.setOpenMenuTabs(res.data.menuTabs === 'enable');
globalStore.updateLanguage(res.data.language); globalStore.updateLanguage(res.data.language);
globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme }); globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName });
globalStore.setThemeConfig({ ...themeConfig.value, panelName: res.data.panelName });
switchDark();
}; };
const loadDataFromXDB = async () => { const loadDataFromXDB = async () => {
const xpackModules = import.meta.globEager('../xpack/api/modules/*.ts'); const res = await getXpackSetting();
if (xpackModules['../xpack/api/modules/setting.ts']) { if (res) {
const searchXSetting = xpackModules['../xpack/api/modules/setting.ts'].searchXSetting; globalStore.setThemeConfig({
if (searchXSetting) { ...themeConfig.value,
const res = await searchXSetting(); title: res.data.title,
globalStore.themeConfig.title = res.data.title; logo: res.data.logo,
globalStore.themeConfig.logo = res.data.logo; logoWithText: res.data.logoWithText,
globalStore.themeConfig.logoWithText = res.data.logoWithText; favicon: res.data.favicon,
globalStore.themeConfig.favicon = res.data.favicon; theme: res.data.theme || 'dark-gold',
} else { });
resetXSetting();
}
} else {
resetXSetting();
} }
initFavicon(); initFavicon();
}; };
@ -116,20 +110,8 @@ const loadProductProFromDB = async () => {
loadDataFromXDB(); loadDataFromXDB();
globalStore.productProExpires = Number(res.data.productPro); globalStore.productProExpires = Number(res.data.productPro);
} else { } else {
globalStore.themeConfig.title = ''; resetXSetting();
globalStore.themeConfig.logo = '';
globalStore.themeConfig.logoWithText = '';
globalStore.themeConfig.favicon = '';
}
};
const updateDarkMode = async (event: MediaQueryListEvent) => {
const res = await getSettingInfo();
if (res.data.theme !== 'auto') {
return;
} }
globalStore.setThemeConfig({ ...themeConfig.value, theme: event.matches ? 'dark' : 'light' });
switchDark();
}; };
const loadStatus = async () => { const loadStatus = async () => {
@ -166,17 +148,7 @@ onMounted(() => {
initFavicon(); initFavicon();
loadDataFromDB(); loadDataFromDB();
loadProductProFromDB(); loadProductProFromDB();
switchTheme();
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
if (mqList.addEventListener) {
mqList.addEventListener('change', (e) => {
updateDarkMode(e);
});
} else if (mqList.addListener) {
mqList.addListener((e) => {
updateDarkMode(e);
});
}
}); });
</script> </script>

4
frontend/src/main.ts

@ -6,6 +6,10 @@ import '@/styles/common.scss';
import '@/assets/iconfont/iconfont.css'; import '@/assets/iconfont/iconfont.css';
import '@/assets/iconfont/iconfont.js'; import '@/assets/iconfont/iconfont.js';
import '@/styles/style.css'; import '@/styles/style.css';
const styleModule = import.meta.glob('xpack/styles/index.scss');
for (const path in styleModule) {
styleModule[path]?.();
}
import directives from '@/directives/index'; import directives from '@/directives/index';
import router from '@/routers/index'; import router from '@/routers/index';

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

@ -39,7 +39,10 @@ const GlobalStore = defineStore({
isProductPro: false, isProductPro: false,
productProExpires: 0, productProExpires: 0,
}), }),
getters: {}, getters: {
isDarkTheme: (state) => state.themeConfig.theme === 'dark' || state.themeConfig.theme === 'dark-gold',
isDarkGoldTheme: (state) => state.themeConfig.theme === 'dark-gold' && state.isProductPro,
},
actions: { actions: {
setOpenMenuTabs(openMenuTabs: boolean) { setOpenMenuTabs(openMenuTabs: boolean) {
this.openMenuTabs = openMenuTabs; this.openMenuTabs = openMenuTabs;

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

@ -335,4 +335,29 @@ html.dark {
.file-item:hover { .file-item:hover {
background-color: #4f4f4f; 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);
}
}
}
} }

18
frontend/src/styles/element.scss

@ -1,3 +1,17 @@
:root {
--el-color-primary: #005eeb;
--el-color-primary-dark-2: #0054d3;
--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;
}
html { html {
--el-box-shadow-light: 0px 0px 4px rgba(0, 94, 235, 0.1) !important; --el-box-shadow-light: 0px 0px 4px rgba(0, 94, 235, 0.1) !important;
@ -206,3 +220,7 @@ html {
vertical-align: text-top !important; vertical-align: text-top !important;
} }
} }
.logo {
color: var(--el-color-primary);
}

2
frontend/src/styles/var.scss

@ -1 +1 @@
$primary-color: #005eeb; $primary-color: var(--el-color-primary);

17
frontend/src/utils/xpack.ts

@ -16,3 +16,20 @@ export function initFavicon() {
link.href = favicon ? '/api/v1/images/favicon' : '/public/favicon.png'; link.href = favicon ? '/api/v1/images/favicon' : '/public/favicon.png';
document.getElementsByTagName('head')[0].appendChild(link); document.getElementsByTagName('head')[0].appendChild(link);
} }
export async function getXpackSetting() {
const searchXSettingGlob = import.meta.glob('xpack/api/modules/setting.ts');
const module = await searchXSettingGlob?.['../xpack/api/modules/setting.ts']?.();
const res = module?.searchXSetting();
if (!res) {
resetXSetting();
return;
}
return res;
}
export async function updateXpackSetting(fromData: FormData) {
const searchXSettingGlob = import.meta.glob('xpack/api/modules/setting.ts');
const module = await searchXSettingGlob?.['../xpack/api/modules/setting.ts']?.();
return module?.updateXSetting(fromData);
}

9
frontend/src/views/app-store/detail/index.vue

@ -38,6 +38,7 @@
v-if="appDetail.enable && operate === 'install'" v-if="appDetail.enable && operate === 'install'"
@click="openInstall" @click="openInstall"
type="primary" type="primary"
class="brief-button"
> >
{{ $t('app.install') }} {{ $t('app.install') }}
</el-button> </el-button>
@ -71,11 +72,7 @@
</div> </div>
</div> </div>
</div> </div>
<MdEditor <MdEditor previewOnly v-model="app.readMe" :theme="isDarkTheme ? 'dark' : 'light'" />
previewOnly
v-model="app.readMe"
:theme="globalStore.$state.themeConfig.theme === 'dark' ? 'dark' : 'light'"
/>
</el-drawer> </el-drawer>
<Install ref="installRef"></Install> <Install ref="installRef"></Install>
</template> </template>
@ -88,7 +85,9 @@ import Install from './install/index.vue';
import router from '@/routers'; import router from '@/routers';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { getLanguage } from '@/utils/util'; import { getLanguage } from '@/utils/util';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const language = getLanguage(); const language = getLanguage();

30
frontend/src/views/login/entrance/index.vue

@ -50,7 +50,7 @@ import ErrDomain from '@/components/error-message/err_domain.vue';
import ErrFound from '@/components/error-message/404.vue'; import ErrFound from '@/components/error-message/404.vue';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { initFavicon, resetXSetting } from '@/utils/xpack'; import { getXpackSetting, initFavicon } from '@/utils/xpack';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const screenWidth = ref(null); const screenWidth = ref(null);
@ -116,28 +116,12 @@ const getStatus = async () => {
}; };
const loadDataFromXDB = async () => { const loadDataFromXDB = async () => {
const xpackModules = import.meta.globEager('../../../xpack/api/modules/*.ts'); const res = await getXpackSetting();
if (xpackModules['../../../xpack/api/modules/setting.ts']) { if (res) {
const searchXSetting = xpackModules['../../../xpack/api/modules/setting.ts'].searchXSetting; globalStore.themeConfig.title = res.data.title;
if (searchXSetting) { globalStore.themeConfig.logo = res.data.logo;
await searchXSetting() globalStore.themeConfig.logoWithText = res.data.logoWithText;
.then((res) => { globalStore.themeConfig.favicon = res.data.favicon;
globalStore.themeConfig.title = res.data.title;
globalStore.themeConfig.logo = res.data.logo;
globalStore.themeConfig.logoWithText = res.data.logoWithText;
globalStore.themeConfig.favicon = res.data.favicon;
})
.catch(() => {
loading.value = false;
resetXSetting();
});
} else {
loading.value = false;
resetXSetting();
}
} else {
loading.value = false;
resetXSetting();
} }
loading.value = false; loading.value = false;
initFavicon(); initFavicon();

30
frontend/src/views/login/index.vue

@ -24,7 +24,7 @@ import LoginForm from './components/login-form.vue';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import router from '@/routers'; import router from '@/routers';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { initFavicon, resetXSetting } from '@/utils/xpack'; import { getXpackSetting, initFavicon } from '@/utils/xpack';
const gStore = GlobalStore(); const gStore = GlobalStore();
const loading = ref(); const loading = ref();
@ -48,28 +48,12 @@ const getStatus = async () => {
}; };
const loadDataFromXDB = async () => { const loadDataFromXDB = async () => {
const xpackModules = import.meta.globEager('../../xpack/api/modules/*.ts'); const res = await getXpackSetting();
if (xpackModules['../../xpack/api/modules/setting.ts']) { if (res) {
const searchXSetting = xpackModules['../../xpack/api/modules/setting.ts'].searchXSetting; gStore.themeConfig.title = res.data.title;
if (searchXSetting) { gStore.themeConfig.logo = res.data.logo;
await searchXSetting() gStore.themeConfig.logoWithText = res.data.logoWithText;
.then((resItem) => { gStore.themeConfig.favicon = res.data.favicon;
gStore.themeConfig.title = resItem.data.title;
gStore.themeConfig.logo = resItem.data.logo;
gStore.themeConfig.logoWithText = resItem.data.logoWithText;
gStore.themeConfig.favicon = resItem.data.favicon;
})
.catch(() => {
loading.value = false;
resetXSetting();
});
} else {
loading.value = false;
resetXSetting();
}
} else {
loading.value = false;
resetXSetting();
} }
loading.value = false; loading.value = false;
initFavicon(); initFavicon();

12
frontend/src/views/setting/about/index.vue

@ -4,7 +4,8 @@
<template #main> <template #main>
<div style="text-align: center; margin-top: 20px"> <div style="text-align: center; margin-top: 20px">
<div style="justify-self: center"> <div style="justify-self: center">
<img style="width: 80px" :src="getLogoUrl()" /> <img v-if="globalStore.themeConfig.logo" style="width: 80px" :src="'/api/v1/images/logo'" />
<PrimaryLogo v-else />
</div> </div>
<h3>{{ globalStore.themeConfig.title || $t('setting.description') }}</h3> <h3>{{ globalStore.themeConfig.title || $t('setting.description') }}</h3>
<h3> <h3>
@ -39,6 +40,7 @@ import { getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import SystemUpgrade from '@/components/system-upgrade/index.vue'; import SystemUpgrade from '@/components/system-upgrade/index.vue';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import PrimaryLogo from '@/assets/images/1panel-logo.svg?component';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const version = ref(); const version = ref();
@ -61,14 +63,6 @@ const toGithubStar = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank', 'noopener,noreferrer'); window.open('https://github.com/1Panel-dev/1Panel', '_blank', 'noopener,noreferrer');
}; };
const getLogoUrl = () => {
if (globalStore.themeConfig.logo) {
return '/api/v1/images/logo';
} else {
return new URL(`../../../assets/images/1panel-logo-light.png`, import.meta.url).href;
}
};
onMounted(() => { onMounted(() => {
search(); search();
getSystemAvailable(); getSystemAvailable();

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

@ -28,6 +28,9 @@
<el-form-item :label="$t('setting.theme')" prop="theme"> <el-form-item :label="$t('setting.theme')" prop="theme">
<el-radio-group @change="onSave('Theme', form.theme)" v-model="form.theme"> <el-radio-group @change="onSave('Theme', form.theme)" v-model="form.theme">
<el-radio-button v-if="isProductPro" value="dark-gold">
<span>{{ $t('xpack.setting.darkGold') }}</span>
</el-radio-button>
<el-radio-button value="light"> <el-radio-button value="light">
<span>{{ $t('setting.light') }}</span> <span>{{ $t('setting.light') }}</span>
</el-radio-button> </el-radio-button>
@ -154,7 +157,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, onMounted, computed } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import { getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting'; import { getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
@ -168,12 +171,16 @@ import PanelName from '@/views/setting/panel/name/index.vue';
import SystemIP from '@/views/setting/panel/systemip/index.vue'; import SystemIP from '@/views/setting/panel/systemip/index.vue';
import Network from '@/views/setting/panel/default-network/index.vue'; import Network from '@/views/setting/panel/default-network/index.vue';
import HideMenu from '@/views/setting/panel/hidemenu/index.vue'; import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
import { storeToRefs } from 'pinia';
import { getXpackSetting, updateXpackSetting } from '@/utils/xpack';
const loading = ref(false); const loading = ref(false);
const i18n = useI18n(); const i18n = useI18n();
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const themeConfig = computed(() => globalStore.themeConfig);
const { switchDark } = useTheme(); const { themeConfig, isProductPro } = storeToRefs(globalStore);
const { switchTheme } = useTheme();
const form = reactive({ const form = reactive({
userName: '', userName: '',
@ -227,7 +234,6 @@ const search = async () => {
form.ntpSite = res.data.ntpSite; form.ntpSite = res.data.ntpSite;
form.panelName = res.data.panelName; form.panelName = res.data.panelName;
form.systemIP = res.data.systemIP; form.systemIP = res.data.systemIP;
form.theme = res.data.theme;
form.menuTabs = res.data.menuTabs; form.menuTabs = res.data.menuTabs;
form.language = res.data.language; form.language = res.data.language;
form.complexityVerification = res.data.complexityVerification; form.complexityVerification = res.data.complexityVerification;
@ -241,6 +247,15 @@ const search = async () => {
const json: Node = JSON.parse(res.data.xpackHideMenu); const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedTitles = getCheckedTitles(json); const checkedTitles = getCheckedTitles(json);
form.proHideMenus = checkedTitles.toString(); form.proHideMenus = checkedTitles.toString();
if (isProductPro.value) {
const xpackRes = await getXpackSetting();
if (xpackRes) {
form.theme = xpackRes.data.theme || 'dark-gold';
return;
}
}
form.theme = res.data.theme;
}; };
function extractTitles(node: Node, result: string[]): void { function extractTitles(node: Node, result: string[]): void {
@ -298,7 +313,21 @@ const onSave = async (key: string, val: any) => {
} }
if (key === 'Theme') { if (key === 'Theme') {
globalStore.setThemeConfig({ ...themeConfig.value, theme: val }); globalStore.setThemeConfig({ ...themeConfig.value, theme: val });
switchDark(); switchTheme();
if (isProductPro.value) {
let formData = new FormData();
formData.append('theme', val);
await updateXpackSetting(formData)
.then(async () => {
loading.value = false;
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
await search();
})
.catch(() => {
loading.value = false;
});
return;
}
} }
if (key === 'MenuTabs') { if (key === 'MenuTabs') {
globalStore.setOpenMenuTabs(val === 'enable'); globalStore.setOpenMenuTabs(val === 'enable');

2
frontend/vite-env.d.ts vendored

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />

5
frontend/vite.config.ts

@ -12,6 +12,7 @@ import DefineOptions from 'unplugin-vue-define-options/vite';
import AutoImport from 'unplugin-auto-import/vite'; import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import svgLoader from 'vite-svg-loader';
const prefix = `monaco-editor/esm/vs`; const prefix = `monaco-editor/esm/vs`;
@ -24,6 +25,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
alias: { alias: {
'@': resolve(__dirname, './src'), '@': resolve(__dirname, './src'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
xpack: resolve(__dirname, './src/xpack'),
}, },
}, },
css: { css: {
@ -76,6 +78,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
}), }),
], ],
}), }),
svgLoader({
defaultImport: 'url',
}),
], ],
esbuild: { esbuild: {
pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [], pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [],

Loading…
Cancel
Save