优化
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;
|
median_value?: number | null;
|
||||||
data_count?: number | null;
|
data_count?: number | null;
|
||||||
};
|
};
|
||||||
|
type ApiBuildingFunctionStatBatchItem = {
|
||||||
|
key?: string;
|
||||||
|
data?: ApiBuildingFunctionStat[];
|
||||||
|
};
|
||||||
type ChartDatum = {
|
type ChartDatum = {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
minValue: number | null;
|
minValue: number | null;
|
||||||
@ -153,13 +157,36 @@ type SelectedContentNode = {
|
|||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatWan(value: number) {
|
function formatNumber(value: number, maximumFractionDigits: number) {
|
||||||
const wanValue = value / 10000;
|
return value.toLocaleString('zh-CN', {
|
||||||
const fractionDigits = Math.abs(wanValue) < 1 ? 4 : 2;
|
|
||||||
return `${wanValue.toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 0,
|
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 {
|
function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum {
|
||||||
@ -354,6 +381,7 @@ function App() {
|
|||||||
const [chartQueryVersion, setChartQueryVersion] = useState(0);
|
const [chartQueryVersion, setChartQueryVersion] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [loadingHint, setLoadingHint] = useState('');
|
||||||
|
|
||||||
const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
|
const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
|
||||||
const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
|
const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
|
||||||
@ -511,6 +539,9 @@ function App() {
|
|||||||
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
||||||
setMetricKey(nextMetricKey);
|
setMetricKey(nextMetricKey);
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setLoadError(null);
|
||||||
|
setLoadingHint('正在重新加载数据');
|
||||||
|
setLoading(true);
|
||||||
setChartQueryVersion((version) => version + 1);
|
setChartQueryVersion((version) => version + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -546,37 +577,45 @@ function App() {
|
|||||||
if (selectedContentNodes.length === 0) {
|
if (selectedContentNodes.length === 0) {
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoadingHint('');
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setLoadingHint('正在加载数据');
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
try {
|
try {
|
||||||
const missingNodes = selectedContentNodes.filter((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
||||||
if (missingNodes.length === 0) {
|
if (!hasAnyMissingNode) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoadingHint('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStatsBatch`, {
|
||||||
missingNodes.map(async (node) => {
|
method: 'POST',
|
||||||
const search = new URLSearchParams({
|
signal: controller.signal,
|
||||||
groupBy: groupKey,
|
headers: {
|
||||||
metric: metricKey,
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
groupBy: groupKey,
|
||||||
|
metric: metricKey,
|
||||||
|
nodes: selectedContentNodes.map((node) => ({
|
||||||
|
key: getSelectionKey(node.contentKey, node.id),
|
||||||
contentKey: node.contentKey,
|
contentKey: node.contentKey,
|
||||||
nodeId: node.id,
|
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) => ({
|
setChartDataBySelection((current) => ({
|
||||||
...current,
|
...current,
|
||||||
...Object.fromEntries(results),
|
...Object.fromEntries(results),
|
||||||
@ -587,6 +626,7 @@ function App() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!controller.signal.aborted) {
|
if (!controller.signal.aborted) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoadingHint('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -838,6 +878,14 @@ function App() {
|
|||||||
interpolation: {
|
interpolation: {
|
||||||
type: 'smooth',
|
type: 'smooth',
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
renderer: ({ datum, yKey, yName }) => ({
|
||||||
|
title: yName,
|
||||||
|
data: [
|
||||||
|
{ label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey, statisticKey) },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
axes: {
|
axes: {
|
||||||
x: {
|
x: {
|
||||||
@ -867,13 +915,7 @@ function App() {
|
|||||||
label: {
|
label: {
|
||||||
color: '#1f2933',
|
color: '#1f2933',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
formatter: ({ value }) =>
|
formatter: ({ value }) => formatChartValue(Number(value), metricKey, statisticKey),
|
||||||
!isCount
|
|
||||||
? formatWan(Number(value))
|
|
||||||
: Number(value).toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -902,7 +944,7 @@ function App() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [chartDataBySelection, selectedContentNodes, selectedMetric.label, selectedStatistic.label, statisticKey]);
|
}, [chartDataBySelection, metricKey, selectedContentNodes, selectedMetric.label, selectedStatistic.label, statisticKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="dashboard-shell">
|
<main className="dashboard-shell">
|
||||||
@ -969,11 +1011,16 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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)">
|
<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" />
|
<i className="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button" />
|
||||||
</button>
|
</button>
|
||||||
<AgCharts options={chartOptions} />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -191,6 +191,29 @@ button {
|
|||||||
height: auto;
|
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 {
|
.chart-frame .ag-charts-wrapper {
|
||||||
--ag-charts-accent-color: #0078a8;
|
--ag-charts-accent-color: #0078a8;
|
||||||
--ag-charts-button-background-color: rgba(255, 249, 241, 0.72);
|
--ag-charts-button-background-color: rgba(255, 249, 241, 0.72);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user