mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
feat: 添加响应式地图缩放和容器大小调整功能,优化性能页面模板样式
This commit is contained in:
@@ -7,7 +7,7 @@ import { ScatterChart } from 'echarts/charts';
|
|||||||
import { GeoComponent, TooltipComponent } from 'echarts/components';
|
import { GeoComponent, TooltipComponent } from 'echarts/components';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { EmptyState } from '@/components/feedback/empty-state';
|
import { EmptyState } from '@/components/feedback/empty-state';
|
||||||
import worldGeoJson from '@/features/dashboard/data/world-geo.json';
|
import worldGeoJson from '@/features/dashboard/data/world-geo.json';
|
||||||
@@ -91,6 +91,9 @@ type CountryRegionDatum = {
|
|||||||
let worldMapRegistrationAttempted = false;
|
let worldMapRegistrationAttempted = false;
|
||||||
let worldMapRegistrationSucceeded = false;
|
let worldMapRegistrationSucceeded = false;
|
||||||
|
|
||||||
|
const baseWorldMapLayoutSizePercent = 180;
|
||||||
|
const baseWorldMapZoom = 1;
|
||||||
|
|
||||||
function buildNodeDetailHref(id?: number | null) {
|
function buildNodeDetailHref(id?: number | null) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return '/node';
|
return '/node';
|
||||||
@@ -248,8 +251,10 @@ export function WorldStageMap({
|
|||||||
nodes: DashboardNodeHealth[];
|
nodes: DashboardNodeHealth[];
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [mapReady, setMapReady] = useState(false);
|
const [mapReady, setMapReady] = useState(false);
|
||||||
const [mapFailed, setMapFailed] = useState(false);
|
const [mapFailed, setMapFailed] = useState(false);
|
||||||
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ready = ensureWorldMapRegistered();
|
const ready = ensureWorldMapRegistered();
|
||||||
@@ -257,6 +262,41 @@ export function WorldStageMap({
|
|||||||
setMapFailed(!ready);
|
setMapFailed(!ready);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = chartContainerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSize = () => {
|
||||||
|
const nextWidth = container.clientWidth;
|
||||||
|
const nextHeight = container.clientHeight;
|
||||||
|
setContainerSize((previous) =>
|
||||||
|
previous.width === nextWidth && previous.height === nextHeight
|
||||||
|
? previous
|
||||||
|
: { width: nextWidth, height: nextHeight },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateSize);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
observer.observe(container);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const mapPalette = useMemo(
|
const mapPalette = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isDark
|
isDark
|
||||||
@@ -389,6 +429,27 @@ export function WorldStageMap({
|
|||||||
});
|
});
|
||||||
}, [mapPalette, nodes]);
|
}, [mapPalette, nodes]);
|
||||||
|
|
||||||
|
const responsiveMapScale = useMemo(() => {
|
||||||
|
const { width, height } = containerSize;
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widthScale = Math.min(Math.max(width / 960, 0.76), 1.18);
|
||||||
|
const heightScale = Math.min(Math.max(height / 520, 0.82), 1.12);
|
||||||
|
|
||||||
|
return Number(Math.min(widthScale, heightScale).toFixed(3));
|
||||||
|
}, [containerSize]);
|
||||||
|
|
||||||
|
const computedLayoutSize = useMemo(
|
||||||
|
() => `${Math.round(baseWorldMapLayoutSizePercent * responsiveMapScale)}%`,
|
||||||
|
[responsiveMapScale],
|
||||||
|
);
|
||||||
|
const computedZoom = useMemo(
|
||||||
|
() => Number((baseWorldMapZoom * responsiveMapScale).toFixed(3)),
|
||||||
|
[responsiveMapScale],
|
||||||
|
);
|
||||||
|
|
||||||
const mapOption = useMemo<EChartsCoreOption>(
|
const mapOption = useMemo<EChartsCoreOption>(
|
||||||
() => ({
|
() => ({
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -431,8 +492,8 @@ export function WorldStageMap({
|
|||||||
roam: false,
|
roam: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
layoutCenter: ['50%', '50%'],
|
layoutCenter: ['50%', '50%'],
|
||||||
layoutSize: '180%',
|
layoutSize: computedLayoutSize,
|
||||||
zoom: 1,
|
zoom: computedZoom,
|
||||||
regions: countryRegions,
|
regions: countryRegions,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
areaColor: mapPalette.areaColor,
|
areaColor: mapPalette.areaColor,
|
||||||
@@ -453,7 +514,8 @@ export function WorldStageMap({
|
|||||||
largeThreshold: 24,
|
largeThreshold: 24,
|
||||||
symbolSize: (value: unknown) => {
|
symbolSize: (value: unknown) => {
|
||||||
const size = Array.isArray(value) && typeof value[2] === 'number' ? value[2] : 1;
|
const size = Array.isArray(value) && typeof value[2] === 'number' ? value[2] : 1;
|
||||||
return Math.max(8, Math.min(18, 8 + Math.log10(size + 1) * 3.6));
|
const responsiveBase = 8 + Math.log10(size + 1) * 3.6;
|
||||||
|
return Math.max(7, Math.min(18, responsiveBase * Math.max(responsiveMapScale, 0.88)));
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -481,7 +543,7 @@ export function WorldStageMap({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[countryRegions, isDark, mapNodes, mapPalette],
|
[computedLayoutSize, computedZoom, countryRegions, isDark, mapNodes, mapPalette, responsiveMapScale],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mapReady) {
|
if (!mapReady) {
|
||||||
@@ -500,20 +562,22 @@ export function WorldStageMap({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactEChartsCore
|
<div ref={chartContainerRef} className="h-full w-full">
|
||||||
echarts={echarts}
|
<ReactEChartsCore
|
||||||
option={mapOption}
|
echarts={echarts}
|
||||||
notMerge
|
option={mapOption}
|
||||||
lazyUpdate
|
notMerge
|
||||||
opts={{ renderer: 'canvas' }}
|
lazyUpdate
|
||||||
onEvents={{
|
opts={{ renderer: 'canvas' }}
|
||||||
click: (params: { data?: MapNodeDatum }) => {
|
onEvents={{
|
||||||
if (params.data?.route) {
|
click: (params: { data?: MapNodeDatum }) => {
|
||||||
router.push(params.data.route);
|
if (params.data?.route) {
|
||||||
}
|
router.push(params.data.route);
|
||||||
},
|
}
|
||||||
}}
|
},
|
||||||
style={{ height: '100%', width: '100%' }}
|
}}
|
||||||
/>
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import {
|
|||||||
|
|
||||||
const settingsQueryKey = ['settings', 'options'] as const;
|
const settingsQueryKey = ['settings', 'options'] as const;
|
||||||
const previewQueryKey = ['performance', 'preview'] as const;
|
const previewQueryKey = ['performance', 'preview'] as const;
|
||||||
|
const templatePanelClassName =
|
||||||
|
'h-[560px] w-full box-border overflow-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 font-mono text-xs leading-6';
|
||||||
|
|
||||||
const defaultPerformanceFields = {
|
const defaultPerformanceFields = {
|
||||||
OpenRestyWorkerProcesses: 'auto',
|
OpenRestyWorkerProcesses: 'auto',
|
||||||
@@ -1266,7 +1268,7 @@ export function PerformancePage() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<ResourceField
|
<ResourceField
|
||||||
label="模板原文"
|
label="模板原文"
|
||||||
@@ -1275,7 +1277,8 @@ export function PerformancePage() {
|
|||||||
<ResourceTextarea
|
<ResourceTextarea
|
||||||
value={templateContent}
|
value={templateContent}
|
||||||
onChange={(event) => setTemplateContent(event.target.value)}
|
onChange={(event) => setTemplateContent(event.target.value)}
|
||||||
className="min-h-[560px] font-mono text-xs leading-6"
|
spellCheck={false}
|
||||||
|
className={`${templatePanelClassName} resize-none`}
|
||||||
placeholder="请输入受控 nginx.conf 模板"
|
placeholder="请输入受控 nginx.conf 模板"
|
||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
@@ -1297,7 +1300,7 @@ export function PerformancePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
className="max-h-[560px] overflow-auto text-xs leading-6 whitespace-pre-wrap">
|
className={`${templatePanelClassName} whitespace-pre-wrap break-words`}>
|
||||||
{preview.main_config}
|
{preview.main_config}
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user