From c413072afc4ae5547e093346560e1a79f4aaeaca Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Fri, 22 May 2026 09:12:38 +0800 Subject: [PATCH] 1 --- .../console-2026-05-19T03-11-51-415Z.log | 2 + .../page-2026-05-19T03-11-52-497Z.yml | 83 ++++ index.html | 2 +- src/App.tsx | 407 ++++++++++++------ src/main.tsx | 2 +- src/styles.css | 26 +- 6 files changed, 393 insertions(+), 129 deletions(-) create mode 100644 .playwright-mcp/console-2026-05-19T03-11-51-415Z.log create mode 100644 .playwright-mcp/page-2026-05-19T03-11-52-497Z.yml diff --git a/.playwright-mcp/console-2026-05-19T03-11-51-415Z.log b/.playwright-mcp/console-2026-05-19T03-11-51-415Z.log new file mode 100644 index 0000000..4ba141c --- /dev/null +++ b/.playwright-mcp/console-2026-05-19T03-11-51-415Z.log @@ -0,0 +1,2 @@ +[ 221ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5179/node_modules/.vite/deps/react-dom_client.js?v=eccfa23d:20102 +[ 1056ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:5179/favicon.ico:0 diff --git a/.playwright-mcp/page-2026-05-19T03-11-52-497Z.yml b/.playwright-mcp/page-2026-05-19T03-11-52-497Z.yml new file mode 100644 index 0000000..4410d58 --- /dev/null +++ b/.playwright-mcp/page-2026-05-19T03-11-52-497Z.yml @@ -0,0 +1,83 @@ +- main [ref=e3]: + - generic: + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - generic: 众为数字化管理平台 + - region "年度费用模板" [ref=e4]: + - generic "筛选条件" [ref=e5]: + - button "省市区" [ref=e6] [cursor=pointer]: + - img [ref=e7] + - generic [ref=e11]: 省市区 + - button "自然地理区位" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - generic [ref=e16]: 自然地理区位 + - button "设施类别" [ref=e17] [cursor=pointer]: + - img [ref=e18] + - generic [ref=e22]: 设施类别 + - button "建设阶段" [ref=e23] [cursor=pointer]: + - img [ref=e24] + - generic [ref=e29]: 建设阶段 + - button "规划形式" [ref=e30] [cursor=pointer]: + - img [ref=e31] + - generic [ref=e36]: 规划形式 + - region "年度总费用图表" [ref=e37]: + - generic [ref=e38]: + - button "纵坐标:造价(元)" [ref=e40] [cursor=pointer]: 造价(元) + - generic [ref=e41]: + - figure "图表,共有0个系列": + - generic [ref=e42]: + - img "interactive chart": + - generic: + - img + - img + - region [ref=e43] + - toolbar "标注" [ref=e44]: + - button "库" [disabled] [ref=e45] [cursor=pointer]: + - generic: + - generic: 库 + - button "指" [disabled] [ref=e46] [cursor=pointer]: + - generic: + - generic: 指 + - button "均" [disabled] [ref=e47] [cursor=pointer]: + - generic: 均 + - button "Line Tool" [disabled] [ref=e48] + - button "Text Tool" [disabled] [ref=e49] + - button "Shape Tool" [disabled] [ref=e50] + - button "Fibonacci Tool" [disabled] [ref=e51] + - button "全屏(F11)" [disabled] [ref=e52] [cursor=pointer] + - button "Clear annotations" [disabled] [ref=e53] + - button "切换到表格" [disabled] [ref=e54] [cursor=pointer]: + - generic: + - generic: 趋 + - status: + - generic: 请选择右侧分类项 + - toolbar "缩放" [ref=e55]: + - button "缩小" [disabled] [ref=e56] + - button "放大" [ref=e57] [cursor=pointer] + - button "左移" [disabled] [ref=e58] + - button "右移" [disabled] [ref=e59] + - button "重置" [disabled] [ref=e60] + - complementary "选择内容" [ref=e61]: + - tablist "选择内容切换项" [ref=e62]: + - tab "自然地理区位" [selected] [ref=e63] [cursor=pointer] + - tab "设施类别" [ref=e64] [cursor=pointer] + - tab "建设阶段" [ref=e65] [cursor=pointer] + - tab "规划形式" [ref=e66] [cursor=pointer] + - generic [ref=e67]: + - generic [ref=e68]: 自然地理区位 + - generic [ref=e69]: Unexpected token '<', "AG Chart Service -
+
diff --git a/src/App.tsx b/src/App.tsx index 8c2205e..19049d1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { AllCommunityModule as AgGridAllCommunityModule, ModuleRegistry as AgGridModuleRegistry, type ColDef, + type ColGroupDef, type ValueFormatterParams, } from 'ag-grid-community'; import type { AgCartesianChartOptions } from 'ag-charts-community'; @@ -120,6 +121,7 @@ const defaultTemplateFilterNode = { filterKey: 'templateLibrary', label: '默认模板', } as const; +const overallSummaryKey = 'summary'; // const mockGeoLocationPayload = { // checkStrictly: true, @@ -161,11 +163,21 @@ type ApiBuildingFunctionStat = { max_value?: number | null; avg_value?: number | null; median_value?: number | null; + threshold_low_value?: number | null; + threshold_center_value?: number | null; + threshold_high_value?: number | null; + stddev_value?: number | null; + standard_deviation?: number | null; + iqr_value?: number | null; + quartile_range?: number | null; + variation_coefficient?: number | null; + coefficient_of_variation?: number | null; data_count?: number | null; }; type ApiBuildingFunctionStatBatchItem = { key?: string; data?: ApiBuildingFunctionStat[]; + summary?: ApiBuildingFunctionStat | null; }; type ChartDatum = { groupName: string; @@ -173,6 +185,12 @@ type ChartDatum = { maxValue: number | null; avgValue: number | null; medianValue: number | null; + thresholdLowValue: number | null; + thresholdCenterValue: number | null; + thresholdHighValue: number | null; + standardDeviation: number | null; + interquartileRange: number | null; + coefficientOfVariation: number | null; dataCount: number | null; }; type TreeNode = { @@ -201,9 +219,17 @@ type SelectedFilterNode = { type PivotGridRow = { year: string; name: string; + summary: boolean; + lowValue: number | null; + centerValue: number | null; + highValue: number | null; maxValue: number | null; minValue: number | null; avgValue: number | null; + medianValue: number | null; + standardDeviation: number | null; + interquartileRange: number | null; + coefficientOfVariation: number | null; dataCount: number | null; }; @@ -240,12 +266,22 @@ function formatChartValue(value: number, metricKey: MetricKey) { } function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum { + const avgValue = row.avg_value ?? null; + const medianValue = row.median_value ?? null; + const fallbackThresholdLowValue = avgValue == null || medianValue == null ? null : Math.min(avgValue, medianValue); + const fallbackThresholdHighValue = avgValue == null || medianValue == null ? null : Math.max(avgValue, medianValue); return { groupName: row.group_name || String(row.group_key ?? '未命名'), minValue: row.min_value ?? null, maxValue: row.max_value ?? null, - avgValue: row.avg_value ?? null, - medianValue: row.median_value ?? null, + avgValue, + medianValue, + thresholdLowValue: row.threshold_low_value ?? fallbackThresholdLowValue, + thresholdCenterValue: row.threshold_center_value ?? medianValue, + thresholdHighValue: row.threshold_high_value ?? fallbackThresholdHighValue, + standardDeviation: row.stddev_value ?? row.standard_deviation ?? null, + interquartileRange: row.iqr_value ?? row.quartile_range ?? null, + coefficientOfVariation: row.variation_coefficient ?? row.coefficient_of_variation ?? null, dataCount: row.data_count ?? null, }; } @@ -686,6 +722,7 @@ function App() { const [statisticKey, setStatisticKey] = useState('avgValue'); const [metricKey, setMetricKey] = useState('cost'); const [chartViewKey, setChartViewKey] = useState('trend'); + const [workspaceFullscreen, setWorkspaceFullscreen] = useState(false); const [statisticMenuOpen, setStatisticMenuOpen] = useState(false); const [metricMenuOpen, setMetricMenuOpen] = useState(false); const [activeContentKey, setActiveContentKey] = useState('geoLocation'); @@ -709,6 +746,7 @@ function App() { }); const [selectedContentNodes, setSelectedContentNodes] = useState([]); const [chartDataBySelection, setChartDataBySelection] = useState>({}); + const [chartSummaryBySelection, setChartSummaryBySelection] = useState>({}); const [chartQueryVersion, setChartQueryVersion] = useState(0); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); @@ -798,6 +836,7 @@ function App() { const currentViewShortLabel = chartViewKey === 'pivot' ? '表' : '趋'; const pivotToggleActionLabel = chartViewKey === 'pivot' ? '切换到趋势图' : '切换到表格'; const pivotToggleTitle = `${chartViewKey === 'pivot' ? '当前表格' : '当前趋势图'},${pivotToggleActionLabel}`; + const fullscreenToggleLabel = workspaceFullscreen ? '退出全屏' : '全屏'; const activeContent = contentOptions.find((option) => option.key === activeContentKey) ?? contentOptions[0]; const activeTree = treeByContent[activeContentKey]; const activeFilter = filterOptions.find((option) => option.key === filterModalKey); @@ -846,15 +885,49 @@ function App() { return rows.map((datum) => ({ year: datum.groupName, name: node.label, + summary: false, + lowValue: datum.thresholdLowValue, + centerValue: datum.thresholdCenterValue, + highValue: datum.thresholdHighValue, maxValue: datum.maxValue, minValue: datum.minValue, avgValue: datum.avgValue, + medianValue: datum.medianValue, + standardDeviation: datum.standardDeviation, + interquartileRange: datum.interquartileRange, + coefficientOfVariation: datum.coefficientOfVariation, dataCount: datum.dataCount, })); }), [chartDataBySelection, selectedContentNodes], ); - const pivotGridColumnDefs = useMemo[]>( + const pivotGridPinnedBottomRowData = useMemo( + () => { + const datum = chartSummaryBySelection[overallSummaryKey] + ?? selectedContentNodes + .map((node) => chartSummaryBySelection[getSelectionKey(node.contentKey, node.id)]) + .find(Boolean); + if (!datum) return []; + return [{ + year: '', + name: '统计', + summary: true, + lowValue: datum.thresholdLowValue, + centerValue: datum.thresholdCenterValue, + highValue: datum.thresholdHighValue, + maxValue: datum.maxValue, + minValue: datum.minValue, + avgValue: datum.avgValue, + medianValue: datum.medianValue, + standardDeviation: datum.standardDeviation, + interquartileRange: datum.interquartileRange, + coefficientOfVariation: datum.coefficientOfVariation, + dataCount: datum.dataCount, + }]; + }, + [chartSummaryBySelection, selectedContentNodes], + ); + const pivotGridColumnDefs = useMemo<(ColDef | ColGroupDef)[]>( () => { const valueFormatter = ({ value }: ValueFormatterParams) => ( value == null ? '' : formatChartValue(Number(value), requestMetricKey) @@ -874,34 +947,86 @@ function App() { minWidth: 108, }, { - field: 'maxValue', - headerName: '最大值', - type: 'numericColumn', - minWidth: 78, - valueFormatter, + headerName: '基准阀值', + children: [ + { + field: 'lowValue', + headerName: '低值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'centerValue', + headerName: '中心值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'highValue', + headerName: '高值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + ], }, { - field: 'minValue', - headerName: '最小值', - type: 'numericColumn', - minWidth: 78, - valueFormatter, - }, - { - field: 'avgValue', - headerName: '平均值', - type: 'numericColumn', - minWidth: 78, - valueFormatter, - }, - { - field: 'dataCount', - headerName: '数量', - type: 'numericColumn', - minWidth: 70, - valueFormatter: ({ value }: ValueFormatterParams) => ( - value == null ? '' : formatChartValue(Number(value), 'dataCount') - ), + headerName: '样本统计值(括号显示样本数量)', + children: [ + { + field: 'maxValue', + headerName: '最大值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'minValue', + headerName: '最小值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'avgValue', + headerName: '平均值', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'medianValue', + headerName: '中位数', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'standardDeviation', + headerName: '标准差', + type: 'numericColumn', + minWidth: 78, + valueFormatter, + }, + { + field: 'interquartileRange', + headerName: '四分位距', + type: 'numericColumn', + minWidth: 88, + valueFormatter, + }, + { + field: 'coefficientOfVariation', + headerName: '变异系数', + type: 'numericColumn', + minWidth: 88, + valueFormatter: ({ value }: ValueFormatterParams) => ( + value == null ? '' : formatNumber(Number(value), 4) + ), + }, + ], }, ]; }, @@ -1172,6 +1297,10 @@ function App() { const { [selectionKey]: _removed, ...rest } = data; return rest; }); + setChartSummaryBySelection((data) => { + const { [selectionKey]: _removed, ...rest } = data; + return rest; + }); return current.filter((_, index) => index !== existingIndex); } @@ -1185,6 +1314,7 @@ function App() { setActiveContentKey(nextContentKey); setSelectedContentNodes([]); setChartDataBySelection({}); + setChartSummaryBySelection({}); setLoadError(null); setLoadingHint(''); setLoading(false); @@ -1416,6 +1546,7 @@ function App() { resetIndicatorTreeState(); } setChartDataBySelection({}); + setChartSummaryBySelection({}); setLoadError(null); if (selectedContentNodes.length > 0) { setLoadingHint('正在按筛选条件重新计算'); @@ -1448,6 +1579,7 @@ function App() { resetIndicatorTreeState(); } setChartDataBySelection({}); + setChartSummaryBySelection({}); setLoadError(null); if (selectedContentNodes.length > 0) { setLoadingHint('正在按筛选条件重新计算'); @@ -1459,6 +1591,7 @@ function App() { const updateMetricKey = (nextMetricKey: MetricKey) => { setMetricKey(nextMetricKey); setChartDataBySelection({}); + setChartSummaryBySelection({}); setLoadError(null); setLoadingHint('正在重新加载数据'); setLoading(true); @@ -1471,12 +1604,33 @@ function App() { setStatisticMenuOpen(false); }, []); + const toggleWorkspaceFullscreen = useCallback(() => { + const fullscreenTarget = workspaceRef.current; + if (!fullscreenTarget) return; + if (document.fullscreenElement === fullscreenTarget) { + void document.exitFullscreen(); + } else { + void fullscreenTarget.requestFullscreen(); + } + }, []); + const openGridFilterModal = (filterKey: 'templateLibrary' | 'indicatorTree') => { setMetricMenuOpen(false); setStatisticMenuOpen(false); openFilterModal(filterKey); }; + useEffect(() => { + const handleFullscreenChange = () => { + setWorkspaceFullscreen(document.fullscreenElement === workspaceRef.current); + }; + handleFullscreenChange(); + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); + useEffect(() => { if (!contentTreeConfigs[activeContentKey]) return; if (treeByContent[activeContentKey].length > 0 || treeInitialLoadStartedRef.current[activeContentKey]) return; @@ -1515,10 +1669,6 @@ function App() { async function loadStats() { if (selectedContentNodes.length === 0) { setChartDataBySelection({}); - setLoading(false); - setLoadingHint(''); - setLoadError(null); - return; } setLoading(true); @@ -1526,7 +1676,7 @@ function App() { setLoadError(null); try { const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]); - if (!hasAnyMissingNode) { + if (selectedContentNodes.length > 0 && !hasAnyMissingNode && chartSummaryBySelection[overallSummaryKey]) { setLoading(false); setLoadingHint(''); return; @@ -1557,10 +1707,19 @@ function App() { const results = (payload.data ?? []) .filter((item) => item.key) .map((item) => [item.key as string, (item.data ?? []).map(normalizeStat).slice(0, 36)] as const); + const summaries = (payload.data ?? []) + .filter((item) => item.key && item.summary) + .map((item) => [item.key as string, normalizeStat(item.summary as ApiBuildingFunctionStat)] as const); + const overallSummary = (payload.data ?? []).find((item) => item.summary)?.summary; setChartDataBySelection((current) => ({ ...current, ...Object.fromEntries(results), })); + setChartSummaryBySelection((current) => ({ + ...current, + ...Object.fromEntries(summaries), + ...(overallSummary ? { [overallSummaryKey]: normalizeStat(overallSummary) } : {}), + })); } catch (error) { if (controller.signal.aborted) return; setLoadError(error instanceof Error ? error.message : '接口请求失败'); @@ -1654,20 +1813,12 @@ function App() { } }; - const toggleFullscreen = () => { - if (document.fullscreenElement === fullscreenTarget) { - void document.exitFullscreen(); - } else { - void fullscreenTarget.requestFullscreen(); - } - }; - const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== 'F11') return; event.preventDefault(); event.stopPropagation(); - toggleFullscreen(); + toggleWorkspaceFullscreen(); }; const handleFullscreenChange = () => { @@ -1689,7 +1840,7 @@ function App() { } else if (button.classList.contains('chart-pivot-button')) { togglePivotView(); } else { - toggleFullscreen(); + toggleWorkspaceFullscreen(); } }; @@ -1762,6 +1913,7 @@ function App() { selectedContentNodes.length, statisticMenuOpen, togglePivotView, + toggleWorkspaceFullscreen, ]); const chartOptions = useMemo(() => { @@ -2073,6 +2225,7 @@ function App() { planningForm: [], }); setChartDataBySelection({}); + setChartSummaryBySelection({}); setLoadError(null); if (selectedContentNodes.length > 0) { setLoadingHint('正在按筛选条件重新计算'); @@ -2138,9 +2291,23 @@ function App() { > {currentViewShortLabel} +