新增自定义背景图片

 新增浅色系背景设定,适配自定义背景图片
🪄 修改图表的渲染方式为SVG
🔧 优化页面路由切换的状态记录
This commit is contained in:
hi2hi 2024-12-20 16:43:48 +00:00
parent 582b367088
commit bf30e14c30
13 changed files with 209 additions and 38 deletions

View File

@ -3,6 +3,8 @@ window.$$nazhuaConfig = {
// freeAmount: '白嫖', // 免费服务的费用名称 // freeAmount: '白嫖', // 免费服务的费用名称
// infinityCycle: '长期有效', // 无限周期名称 // infinityCycle: '长期有效', // 无限周期名称
// buyBtnText: '购买', // 购买按钮文案 // buyBtnText: '购买', // 购买按钮文案
// customBackgroundImage: '', // 自定义的背景图片地址
// lightBackground: true, // 启用了浅色系背景图,会强制关闭点点背景
// listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式移动端自动切换至card // listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式移动端自动切换至card
// listServerStatusType: 'progress', // 服务器状态类型--列表 // listServerStatusType: 'progress', // 服务器状态类型--列表
// listServerRealTimeShowLoad: false, // 列表显示服务器实时负载 // listServerRealTimeShowLoad: false, // 列表显示服务器实时负载

View File

@ -185,6 +185,8 @@ window.$$nazhuaConfig = {
freeAmount: '白嫖', // 免费服务的费用名称 freeAmount: '白嫖', // 免费服务的费用名称
infinityCycle: '长期有效', // 无限周期名称 infinityCycle: '长期有效', // 无限周期名称
buyBtnText: '购买', // 购买按钮文案 buyBtnText: '购买', // 购买按钮文案
customBackgroundImage: '', // 自定义的背景图片地址
lightBackground: true, // 启用了浅色系背景图,会强制关闭点点背景
listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式目前不兼容移动端 listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式目前不兼容移动端
listServerStatusType: 'progress', // 服务器状态类型--列表 listServerStatusType: 'progress', // 服务器状态类型--列表
listServerRealTimeShowLoad: false, // 列表显示服务器实时负载 listServerRealTimeShowLoad: false, // 列表显示服务器实时负载

View File

@ -1,6 +1,10 @@
<template> <template>
<layout-main> <layout-main>
<router-view /> <router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</layout-main> </layout-main>
</template> </template>

View File

@ -1,5 +1,5 @@
import { use } from 'echarts/core'; import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers'; import { SVGRenderer } from 'echarts/renderers';
import { import {
BarChart, BarChart,
} from 'echarts/charts'; } from 'echarts/charts';
@ -10,7 +10,7 @@ import {
import config from '@/config'; import config from '@/config';
use([ use([
CanvasRenderer, SVGRenderer,
BarChart, BarChart,
PolarComponent, PolarComponent,
]); ]);
@ -80,8 +80,16 @@ export default (used, total, itemColors, size = 100) => ({
itemStyle: { itemStyle: {
color: typeof itemColors === 'string' ? itemColors : handleColor(itemColors?.used), color: typeof itemColors === 'string' ? itemColors : handleColor(itemColors?.used),
borderRadius: 5, borderRadius: 5,
shadowColor: config.nazhua.serverStatusLinear ? 'rgba(0, 0, 0, 0.5)' : undefined, shadowColor: (() => {
shadowBlur: config.nazhua.serverStatusLinear ? 10 : undefined, if (config.nazhua.serverStatusLinear) {
return 'rgba(0, 0, 0, 0.5)';
}
if (config.nazhua.lightBackground) {
return 'rgba(0, 0, 0, 0.2)';
}
return undefined;
})(),
shadowBlur: (config.nazhua.serverStatusLinear || config.nazhua.lightBackground) ? 10 : undefined,
}, },
coordinateSystem: 'polar', coordinateSystem: 'polar',
cursor: 'default', cursor: 'default',

View File

@ -1,5 +1,5 @@
import { use } from 'echarts/core'; import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers'; import { SVGRenderer } from 'echarts/renderers';
import { LineChart } from 'echarts/charts'; import { LineChart } from 'echarts/charts';
import { import {
TooltipComponent, TooltipComponent,
@ -12,7 +12,7 @@ import dayjs from 'dayjs';
import config from '@/config'; import config from '@/config';
use([ use([
CanvasRenderer, SVGRenderer,
LineChart, LineChart,
TooltipComponent, TooltipComponent,
// LegendComponent, // LegendComponent,

View File

@ -2,7 +2,7 @@
<div <div
class="dot-dot-box" class="dot-dot-box"
:class="{ :class="{
'dot-dot-box--hide': $config.nazhua?.hideDotBG === true, 'dot-dot-box--hide': hideDotBG,
}" }"
:style="boxStyle" :style="boxStyle"
> >
@ -16,6 +16,7 @@
*/ */
import { computed } from 'vue'; import { computed } from 'vue';
import config from '@/config';
const props = defineProps({ const props = defineProps({
borderRadius: { borderRadius: {
@ -32,6 +33,10 @@ const props = defineProps({
}, },
}); });
const lightBackground = computed(() => config.nazhua.lightBackground);
const hideDotBG = computed(() => lightBackground.value || config.nazhua?.hideDotBG === true);
const boxStyle = computed(() => { const boxStyle = computed(() => {
const style = {}; const style = {};
if (props.borderRadius) { if (props.borderRadius) {
@ -68,9 +73,14 @@ const boxStyle = computed(() => {
backdrop-filter: saturate(50%) blur(3px); backdrop-filter: saturate(50%) blur(3px);
&--hide { &--hide {
background-color: rgba(#000, 0.8); background-color: rgba(#000, 0.5);
background-image: none; background-image: none;
backdrop-filter: none; backdrop-filter: none;
transition: all 0.3s linear;
&:hover {
background-color: rgba(#000, 0.8);
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {

View File

@ -1,6 +1,9 @@
<template> <template>
<div <div
class="world-map-group" class="world-map-group"
:class="{
'world-map-group--light-background': lightBackground,
}"
:style="mapStyle" :style="mapStyle"
> >
<div class="world-map-img" /> <div class="world-map-img" />
@ -39,6 +42,7 @@ import {
computed, computed,
watch, watch,
} from 'vue'; } from 'vue';
import config from '@/config';
import validate from '@/utils/validate'; import validate from '@/utils/validate';
import WorldMapPoint from './world-map-point.vue'; import WorldMapPoint from './world-map-point.vue';
@ -63,37 +67,46 @@ const props = defineProps({
}, },
}); });
const lightBackground = computed(() => config.nazhua.lightBackground);
const boxPadding = computed(() => (lightBackground.value ? 20 : 0));
// 1280:621 // 1280:621
const computedSize = computed(() => { const computedSize = computed(() => {
// padding
const adjustedWidth = Number(props.width) - (boxPadding.value * 2);
const adjustedHeight = Number(props.height) - (boxPadding.value * 2);
if (!validate.isEmpty(props.width) && !validate.isEmpty(props.height)) { if (!validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
return { return {
width: 1280, width: 1280,
height: 621, height: 621,
}; };
} }
const width = Number(props.width);
const height = Number(props.height);
if (!validate.isEmpty(props.width) && validate.isEmpty(props.height)) { if (!validate.isEmpty(props.width) && validate.isEmpty(props.height)) {
return { return {
width, width: adjustedWidth,
height: Math.ceil((621 / 1280) * width), height: Math.ceil((621 / 1280) * adjustedWidth),
}; };
} }
if (validate.isEmpty(props.width) && !validate.isEmpty(props.height)) { if (validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
return { return {
width: Math.ceil((1280 / 621) * height), width: Math.ceil((1280 / 621) * adjustedHeight),
height, height: adjustedHeight,
}; };
} }
if (width / height > 1280 / 621) {
if (adjustedWidth / adjustedHeight > 1280 / 621) {
return { return {
width: Math.ceil(height * (1280 / 621)), width: Math.ceil(adjustedHeight * (1280 / 621)),
height, height: adjustedHeight,
}; };
} }
return { return {
width, width: adjustedWidth,
height: Math.ceil(width * (621 / 1280)), height: Math.ceil(adjustedWidth * (621 / 1280)),
}; };
}); });
@ -118,8 +131,8 @@ function computeMapPoints() {
const points = props.locations.map((i) => { const points = props.locations.map((i) => {
const item = { const item = {
key: i.key, key: i.key,
left: (computedSize.value.width / 1280) * i.x, left: (computedSize.value.width / 1280) * i.x + boxPadding.value,
top: (computedSize.value.height / 621) * i.y, top: (computedSize.value.height / 621) * i.y + boxPadding.value,
size: i.size || 4, size: i.size || 4,
label: i.label, label: i.label,
servers: i.servers, servers: i.servers,
@ -219,6 +232,30 @@ function handlePointTap(e) {
height: var(--world-map-height, 621px); height: var(--world-map-height, 621px);
position: relative; position: relative;
&--light-background {
padding: 20px;
background: rgba(#000, 0.6);
border-radius: 12px;
box-sizing: content-box;
transition: background-color 0.3s linear;
.world-map-img {
opacity: 1;
}
&:hover {
background: rgba(#000, 0.9);
}
@media screen and (max-width: 768px) {
background: rgba(#000, 0.8);
&:hover {
background: rgba(#000, 0.8);
}
}
}
.world-map-img { .world-map-img {
width: var(--world-map-width, 1280px); width: var(--world-map-width, 1280px);
height: var(--world-map-height, 621px); height: var(--world-map-height, 621px);

View File

@ -143,6 +143,8 @@ const store = useStore();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const lightBackground = computed(() => config.nazhua.lightBackground);
const headerStyle = computed(() => { const headerStyle = computed(() => {
const style = {}; const style = {};
if (route.name === 'ServerDetail') { if (route.name === 'ServerDetail') {
@ -267,6 +269,9 @@ const headerClass = computed(() => {
if (showServerCount.value) { if (showServerCount.value) {
classes.push('layout-header--show-server-count'); classes.push('layout-header--show-server-count');
} }
if (lightBackground.value) {
classes.push('layout-header--light-background');
}
return classes; return classes;
}); });
@ -304,6 +309,12 @@ const dashboardUrl = computed(() => config.nazhua.v1DashboardUrl || '/dashboard'
} }
} }
&--light-background {
background-color: rgba(#000, 0.7);
background-image: none;
backdrop-filter: none;
}
.site-name { .site-name {
line-height: calc(var(--layout-header-height) - 20px); line-height: calc(var(--layout-header-height) - 20px);
font-size: 24px; font-size: 24px;

View File

@ -1,6 +1,12 @@
<template> <template>
<div class="layout-group"> <div
<div class="layout-bg" /> class="layout-group"
:style="layoutGroupStyle"
>
<div
class="layout-bg"
:style="layoutBGStyle"
/>
<div class="layout-main"> <div class="layout-main">
<layout-header /> <layout-header />
<slot /> <slot />
@ -13,8 +19,27 @@
/** /**
* LayoutMain * LayoutMain
*/ */
import { computed } from 'vue';
import config from '@/config';
import LayoutHeader from './components/header.vue'; import LayoutHeader from './components/header.vue';
import LayoutFooter from './components/footer.vue'; import LayoutFooter from './components/footer.vue';
const layoutGroupStyle = computed(() => {
const style = {};
if (config.nazhua.lightBackground) {
style['--layout-main-bg-color'] = 'rgba(20, 30, 40, 0.2)';
}
return style;
});
const layoutBGStyle = computed(() => {
const style = {};
if (config.nazhua.customBackgroundImage) {
style.background = `url(${config.nazhua.customBackgroundImage}) 50% 50%`;
style.backgroundSize = 'cover';
}
return style;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -27,10 +27,15 @@ const constantRoutes = [{
const routerOptions = { const routerOptions = {
history: config.nazhua.routeMode === 'h5' ? createWebHistory() : createWebHashHistory(), history: config.nazhua.routeMode === 'h5' ? createWebHistory() : createWebHashHistory(),
scrollBehavior: () => ({ scrollBehavior: (to, from, savedPosition) => {
top: 0, if (savedPosition) {
behavior: 'smooth', return savedPosition;
}), }
return {
top: 0,
behavior: 'smooth',
};
},
routes: constantRoutes, routes: constantRoutes,
}; };
const router = createRouter(routerOptions); const router = createRouter(routerOptions);

View File

@ -1,5 +1,10 @@
<template> <template>
<div class="server-option-box"> <div
class="server-option-box"
:class="{
'server-option-box--light-background': lightBackground,
}"
>
<div <div
v-for="item in options" v-for="item in options"
:key="item.key" :key="item.key"
@ -22,6 +27,7 @@
import { import {
computed, computed,
} from 'vue'; } from 'vue';
import config from '@/config';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -42,6 +48,8 @@ const emits = defineEmits([
'update:modelValue', 'update:modelValue',
]); ]);
const lightBackground = computed(() => config.nazhua.lightBackground);
const activeValue = computed({ const activeValue = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => { set: (val) => {
@ -75,6 +83,7 @@ function toggleModelValue(item) {
line-height: 1.2; line-height: 1.2;
border-radius: 6px; border-radius: 6px;
background: rgba(#000, 0.3); background: rgba(#000, 0.3);
transition: all 0.3s linear;
cursor: pointer; cursor: pointer;
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
background-color: rgba(#000, 0.8); background-color: rgba(#000, 0.8);
@ -83,10 +92,35 @@ function toggleModelValue(item) {
.option-label { .option-label {
color: #fff; color: #fff;
transition: all 0.3s linear;
}
@media screen and (min-width: 768px) {
&:hover {
.option-label {
color: #ff7500;
}
}
} }
&.active { &.active {
background: rgba(#ff7500, 0.75); background: rgba(#ff7500, 0.75);
.option-label {
color: #fff;
}
}
}
@media screen and (min-width: 768px) {
&--light-background {
.server-option-item {
background: rgba(#000, 0.5);
&:hover {
background: rgba(#000, 0.8);
}
}
} }
} }
} }

View File

@ -13,6 +13,10 @@ export default (params) => {
if (!props?.info) { if (!props?.info) {
return {}; return {};
} }
const lightBackground = computed(() => config.nazhua.lightBackground);
const serverStatusLinear = computed(() => config.nazhua.serverStatusLinear || lightBackground.value);
const cpuInfo = computed(() => { const cpuInfo = computed(() => {
if (props.info?.Host?.CPU?.[0]) { if (props.info?.Host?.CPU?.[0]) {
return hostUtils.getCPUInfo(props.info.Host.CPU[0]); return hostUtils.getCPUInfo(props.info.Host.CPU[0]);
@ -60,11 +64,12 @@ export default (params) => {
* 状态列表 * 状态列表
*/ */
const serverStatusList = computed(() => statusListTpl.split(',').map((i) => { const serverStatusList = computed(() => statusListTpl.split(',').map((i) => {
const totalColor = lightBackground.value ? 'rgba(125, 125, 125, 0.5)' : 'rgba(255, 255, 255, 0.25)';
switch (i) { switch (i) {
case 'cpu': case 'cpu':
{ {
const CoresVal = cpuInfo.value?.cores ? `${cpuInfo.value?.cores}C` : '-'; const CoresVal = cpuInfo.value?.cores ? `${cpuInfo.value?.cores}C` : '-';
const usedColor = config.nazhua.serverStatusLinear ? ['#0088FF', '#72B7FF'] : '#0088FF'; const usedColor = serverStatusLinear.value ? ['#0088FF', '#72B7FF'] : '#0088FF';
const valPercent = `${(props.info.State?.CPU || 0).toFixed(1) * 1}%`; const valPercent = `${(props.info.State?.CPU || 0).toFixed(1) * 1}%`;
const valText = valPercent; const valText = valPercent;
return { return {
@ -72,7 +77,7 @@ export default (params) => {
used: (props.info.State?.CPU || 0).toFixed(1) * 1, used: (props.info.State?.CPU || 0).toFixed(1) * 1,
colors: { colors: {
used: usedColor, used: usedColor,
total: 'rgba(255, 255, 255, 0.25)', total: totalColor,
}, },
valText, valText,
valPercent, valPercent,
@ -97,13 +102,13 @@ export default (params) => {
} else { } else {
contentVal = `${Math.ceil(useMemAndTotalMem.value.total.m)}M`; contentVal = `${Math.ceil(useMemAndTotalMem.value.total.m)}M`;
} }
const usedColor = config.nazhua.serverStatusLinear ? ['#2B6939', '#0AA344'] : '#0AA344'; const usedColor = serverStatusLinear.value ? ['#2B6939', '#0AA344'] : '#0AA344';
return { return {
type: 'mem', type: 'mem',
used: useMemAndTotalMem.value.usePercent, used: useMemAndTotalMem.value.usePercent,
colors: { colors: {
used: usedColor, used: usedColor,
total: 'rgba(255, 255, 255, 0.25)', total: totalColor,
}, },
valText, valText,
valPercent: `${useMemAndTotalMem.value.usePercent.toFixed(1) * 1}%`, valPercent: `${useMemAndTotalMem.value.usePercent.toFixed(1) * 1}%`,
@ -131,13 +136,13 @@ export default (params) => {
} else { } else {
contentVal = `${Math.ceil(useSwapAndTotalSwap.value.total.m)}M`; contentVal = `${Math.ceil(useSwapAndTotalSwap.value.total.m)}M`;
} }
const usedColor = config.nazhua.serverStatusLinear ? ['#FF8C00', '#F38100'] : '#FF8C00'; const usedColor = serverStatusLinear.value ? ['#FF8C00', '#F38100'] : '#FF8C00';
return { return {
type: 'swap', type: 'swap',
used: useSwapAndTotalSwap.value.usePercent, used: useSwapAndTotalSwap.value.usePercent,
colors: { colors: {
used: usedColor, used: usedColor,
total: 'rgba(255, 255, 255, 0.25)', total: totalColor,
}, },
valText, valText,
valPercent: `${useSwapAndTotalSwap.value.usePercent.toFixed(1) * 1}%`, valPercent: `${useSwapAndTotalSwap.value.usePercent.toFixed(1) * 1}%`,
@ -150,21 +155,27 @@ export default (params) => {
} }
case 'disk': case 'disk':
{ {
let valText;
if (useDiskAndTotalDisk.value.used.t >= 1 && useDiskAndTotalDisk.value.total.t >= 1) {
valText = `${(useDiskAndTotalDisk.value.used.t).toFixed(1) * 1}T`;
} else {
valText = `${Math.ceil(useDiskAndTotalDisk.value.used.g)}G`;
}
let contentValue; let contentValue;
if (useDiskAndTotalDisk.value.total.t >= 1) { if (useDiskAndTotalDisk.value.total.t >= 1) {
contentValue = `${(useDiskAndTotalDisk.value.total.t).toFixed(1) * 1}T`; contentValue = `${(useDiskAndTotalDisk.value.total.t).toFixed(1) * 1}T`;
} else { } else {
contentValue = `${Math.ceil(useDiskAndTotalDisk.value.total.g)}G`; contentValue = `${Math.ceil(useDiskAndTotalDisk.value.total.g)}G`;
} }
const usedColor = config.nazhua.serverStatusLinear ? ['#00848F', '#70F3FF'] : '#70F3FF'; const usedColor = serverStatusLinear.value ? ['#00848F', '#70F3FF'] : '#70F3FF';
return { return {
type: 'disk', type: 'disk',
used: useDiskAndTotalDisk.value.usePercent, used: useDiskAndTotalDisk.value.usePercent,
colors: { colors: {
used: usedColor, used: usedColor,
total: 'rgba(255, 255, 255, 0.25)', total: totalColor,
}, },
valText: `${(useDiskAndTotalDisk.value.used.g).toFixed(1) * 1}G`, valText,
valPercent: `${useDiskAndTotalDisk.value.usePercent.toFixed(1) * 1}%`, valPercent: `${useDiskAndTotalDisk.value.usePercent.toFixed(1) * 1}%`,
label: '磁盘', label: '磁盘',
content: { content: {

View File

@ -74,6 +74,9 @@ import {
computed, computed,
onMounted, onMounted,
onUnmounted, onUnmounted,
onActivated,
onDeactivated,
nextTick,
} from 'vue'; } from 'vue';
import { import {
useStore, useStore,
@ -279,6 +282,25 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}); });
const scrollPosition = ref(0);
onDeactivated(() => {
//
scrollPosition.value = document.documentElement.scrollTop || document.body.scrollTop;
});
onActivated(() => {
//
if (scrollPosition.value > 0) {
nextTick(() => {
window.scrollTo({
top: scrollPosition.value,
behavior: 'instant',
});
});
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>