优化
This commit is contained in:
parent
0990959e82
commit
38978befb7
111
src/App.tsx
111
src/App.tsx
@ -128,6 +128,10 @@ type ApiBuildingFunctionStat = {
|
||||
median_value?: number | null;
|
||||
data_count?: number | null;
|
||||
};
|
||||
type ApiBuildingFunctionStatBatchItem = {
|
||||
key?: string;
|
||||
data?: ApiBuildingFunctionStat[];
|
||||
};
|
||||
type ChartDatum = {
|
||||
groupName: string;
|
||||
minValue: number | null;
|
||||
@ -153,13 +157,36 @@ type SelectedContentNode = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
function formatWan(value: number) {
|
||||
const wanValue = value / 10000;
|
||||
const fractionDigits = Math.abs(wanValue) < 1 ? 4 : 2;
|
||||
return `${wanValue.toLocaleString('zh-CN', {
|
||||
function formatNumber(value: number, maximumFractionDigits: number) {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: fractionDigits,
|
||||
})}万`;
|
||||
maximumFractionDigits,
|
||||
});
|
||||
}
|
||||
|
||||
function formatCostValue(value: number) {
|
||||
const absValue = Math.abs(value);
|
||||
if (absValue >= 10000) {
|
||||
return `${formatNumber(value / 10000, 2)}万元`;
|
||||
}
|
||||
const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2;
|
||||
return `${formatNumber(value, fractionDigits)}元`;
|
||||
}
|
||||
|
||||
function formatAreaMetricValue(value: number) {
|
||||
const absValue = Math.abs(value);
|
||||
const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2;
|
||||
return `${formatNumber(value, fractionDigits)}元/m²`;
|
||||
}
|
||||
|
||||
function formatChartValue(value: number, metricKey: MetricKey, statisticKey: StatisticKey) {
|
||||
if (statisticKey === 'dataCount') {
|
||||
return formatNumber(value, 0);
|
||||
}
|
||||
if (metricKey === 'cost') {
|
||||
return formatCostValue(value);
|
||||
}
|
||||
return formatAreaMetricValue(value);
|
||||
}
|
||||
|
||||
function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum {
|
||||
@ -354,6 +381,7 @@ function App() {
|
||||
const [chartQueryVersion, setChartQueryVersion] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [loadingHint, setLoadingHint] = useState('');
|
||||
|
||||
const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
|
||||
const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
|
||||
@ -511,6 +539,9 @@ function App() {
|
||||
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
||||
setMetricKey(nextMetricKey);
|
||||
setChartDataBySelection({});
|
||||
setLoadError(null);
|
||||
setLoadingHint('正在重新加载数据');
|
||||
setLoading(true);
|
||||
setChartQueryVersion((version) => version + 1);
|
||||
};
|
||||
|
||||
@ -546,37 +577,45 @@ function App() {
|
||||
if (selectedContentNodes.length === 0) {
|
||||
setChartDataBySelection({});
|
||||
setLoading(false);
|
||||
setLoadingHint('');
|
||||
setLoadError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setLoadingHint('正在加载数据');
|
||||
setLoadError(null);
|
||||
try {
|
||||
const missingNodes = selectedContentNodes.filter((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
||||
if (missingNodes.length === 0) {
|
||||
const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
||||
if (!hasAnyMissingNode) {
|
||||
setLoading(false);
|
||||
setLoadingHint('');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
missingNodes.map(async (node) => {
|
||||
const search = new URLSearchParams({
|
||||
groupBy: groupKey,
|
||||
metric: metricKey,
|
||||
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStatsBatch`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
groupBy: groupKey,
|
||||
metric: metricKey,
|
||||
nodes: selectedContentNodes.map((node) => ({
|
||||
key: getSelectionKey(node.contentKey, node.id),
|
||||
contentKey: node.contentKey,
|
||||
nodeId: node.id,
|
||||
});
|
||||
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStats?${search.toString()}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const payload = (await response.json()) as { data?: ApiBuildingFunctionStat[] };
|
||||
return [getSelectionKey(node.contentKey, node.id), (payload.data ?? []).map(normalizeStat).slice(0, 36)] as const;
|
||||
})),
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const payload = (await response.json()) as { data?: ApiBuildingFunctionStatBatchItem[] };
|
||||
const results = (payload.data ?? [])
|
||||
.filter((item) => item.key)
|
||||
.map((item) => [item.key as string, (item.data ?? []).map(normalizeStat).slice(0, 36)] as const);
|
||||
setChartDataBySelection((current) => ({
|
||||
...current,
|
||||
...Object.fromEntries(results),
|
||||
@ -587,6 +626,7 @@ function App() {
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
setLoadingHint('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -838,6 +878,14 @@ function App() {
|
||||
interpolation: {
|
||||
type: 'smooth',
|
||||
},
|
||||
tooltip: {
|
||||
renderer: ({ datum, yKey, yName }) => ({
|
||||
title: yName,
|
||||
data: [
|
||||
{ label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey, statisticKey) },
|
||||
],
|
||||
}),
|
||||
},
|
||||
})),
|
||||
axes: {
|
||||
x: {
|
||||
@ -867,13 +915,7 @@ function App() {
|
||||
label: {
|
||||
color: '#1f2933',
|
||||
fontSize: 12,
|
||||
formatter: ({ value }) =>
|
||||
!isCount
|
||||
? formatWan(Number(value))
|
||||
: Number(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}),
|
||||
formatter: ({ value }) => formatChartValue(Number(value), metricKey, statisticKey),
|
||||
},
|
||||
line: {
|
||||
enabled: false,
|
||||
@ -902,7 +944,7 @@ function App() {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
}, [chartDataBySelection, selectedContentNodes, selectedMetric.label, selectedStatistic.label, statisticKey]);
|
||||
}, [chartDataBySelection, metricKey, selectedContentNodes, selectedMetric.label, selectedStatistic.label, statisticKey]);
|
||||
|
||||
return (
|
||||
<main className="dashboard-shell">
|
||||
@ -969,11 +1011,16 @@ function App() {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{loading || loadError ? <div className="chart-status">{loading ? '加载中' : loadError}</div> : null}
|
||||
{loading || loadError ? <div className="chart-status">{loading ? loadingHint || '加载中' : loadError}</div> : null}
|
||||
<button className="chart-fullscreen-button ag-charts-toolbar__button" type="button" title="全屏(F11)">
|
||||
<i className="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button" />
|
||||
</button>
|
||||
<AgCharts options={chartOptions} />
|
||||
{loading ? (
|
||||
<div className="chart-loading-mask" aria-live="polite" aria-busy="true">
|
||||
<div className="chart-loading-panel">{loadingHint || '加载中'}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -191,6 +191,29 @@ button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chart-loading-mask {
|
||||
position: absolute;
|
||||
inset: 26px 0 0;
|
||||
z-index: 13;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 248, 240, 0.58);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.chart-loading-panel {
|
||||
min-width: 120px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(90, 82, 72, 0.16);
|
||||
border-radius: 3px;
|
||||
color: #262a33;
|
||||
background: rgba(255, 252, 248, 0.96);
|
||||
box-shadow: 0 6px 18px rgba(69, 54, 36, 0.12);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chart-frame .ag-charts-wrapper {
|
||||
--ag-charts-accent-color: #0078a8;
|
||||
--ag-charts-button-background-color: rgba(255, 249, 241, 0.72);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user