mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
feat: 添加响应式地图缩放和容器大小调整功能,优化性能页面模板样式
This commit is contained in:
@@ -7,7 +7,7 @@ import { ScatterChart } from 'echarts/charts';
|
||||
import { GeoComponent, TooltipComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
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 worldGeoJson from '@/features/dashboard/data/world-geo.json';
|
||||
@@ -91,6 +91,9 @@ type CountryRegionDatum = {
|
||||
let worldMapRegistrationAttempted = false;
|
||||
let worldMapRegistrationSucceeded = false;
|
||||
|
||||
const baseWorldMapLayoutSizePercent = 180;
|
||||
const baseWorldMapZoom = 1;
|
||||
|
||||
function buildNodeDetailHref(id?: number | null) {
|
||||
if (!id) {
|
||||
return '/node';
|
||||
@@ -248,8 +251,10 @@ export function WorldStageMap({
|
||||
nodes: DashboardNodeHealth[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const chartContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [mapFailed, setMapFailed] = useState(false);
|
||||
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const ready = ensureWorldMapRegistered();
|
||||
@@ -257,6 +262,41 @@ export function WorldStageMap({
|
||||
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(
|
||||
() =>
|
||||
isDark
|
||||
@@ -389,6 +429,27 @@ export function WorldStageMap({
|
||||
});
|
||||
}, [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>(
|
||||
() => ({
|
||||
animation: false,
|
||||
@@ -431,8 +492,8 @@ export function WorldStageMap({
|
||||
roam: false,
|
||||
silent: true,
|
||||
layoutCenter: ['50%', '50%'],
|
||||
layoutSize: '180%',
|
||||
zoom: 1,
|
||||
layoutSize: computedLayoutSize,
|
||||
zoom: computedZoom,
|
||||
regions: countryRegions,
|
||||
itemStyle: {
|
||||
areaColor: mapPalette.areaColor,
|
||||
@@ -453,7 +514,8 @@ export function WorldStageMap({
|
||||
largeThreshold: 24,
|
||||
symbolSize: (value: unknown) => {
|
||||
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: {
|
||||
show: false,
|
||||
@@ -481,7 +543,7 @@ export function WorldStageMap({
|
||||
},
|
||||
],
|
||||
}),
|
||||
[countryRegions, isDark, mapNodes, mapPalette],
|
||||
[computedLayoutSize, computedZoom, countryRegions, isDark, mapNodes, mapPalette, responsiveMapScale],
|
||||
);
|
||||
|
||||
if (!mapReady) {
|
||||
@@ -500,20 +562,22 @@ export function WorldStageMap({
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactEChartsCore
|
||||
echarts={echarts}
|
||||
option={mapOption}
|
||||
notMerge
|
||||
lazyUpdate
|
||||
opts={{ renderer: 'canvas' }}
|
||||
onEvents={{
|
||||
click: (params: { data?: MapNodeDatum }) => {
|
||||
if (params.data?.route) {
|
||||
router.push(params.data.route);
|
||||
}
|
||||
},
|
||||
}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
<div ref={chartContainerRef} className="h-full w-full">
|
||||
<ReactEChartsCore
|
||||
echarts={echarts}
|
||||
option={mapOption}
|
||||
notMerge
|
||||
lazyUpdate
|
||||
opts={{ renderer: 'canvas' }}
|
||||
onEvents={{
|
||||
click: (params: { data?: MapNodeDatum }) => {
|
||||
if (params.data?.route) {
|
||||
router.push(params.data.route);
|
||||
}
|
||||
},
|
||||
}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
|
||||
const settingsQueryKey = ['settings', 'options'] 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 = {
|
||||
OpenRestyWorkerProcesses: 'auto',
|
||||
@@ -1266,7 +1268,7 @@ export function PerformancePage() {
|
||||
</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">
|
||||
<ResourceField
|
||||
label="模板原文"
|
||||
@@ -1275,7 +1277,8 @@ export function PerformancePage() {
|
||||
<ResourceTextarea
|
||||
value={templateContent}
|
||||
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 模板"
|
||||
/>
|
||||
</ResourceField>
|
||||
@@ -1297,7 +1300,7 @@ export function PerformancePage() {
|
||||
/>
|
||||
</div>
|
||||
<CodeBlock
|
||||
className="max-h-[560px] overflow-auto text-xs leading-6 whitespace-pre-wrap">
|
||||
className={`${templatePanelClassName} whitespace-pre-wrap break-words`}>
|
||||
{preview.main_config}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user