1
This commit is contained in:
parent
1d12668e9b
commit
c413072afc
2
.playwright-mcp/console-2026-05-19T03-11-51-415Z.log
Normal file
2
.playwright-mcp/console-2026-05-19T03-11-51-415Z.log
Normal file
@ -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
|
||||||
83
.playwright-mcp/page-2026-05-19T03-11-52-497Z.yml
Normal file
83
.playwright-mcp/page-2026-05-19T03-11-52-497Z.yml
Normal file
@ -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 '<', "<!doctype "... is not valid JSON
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<title>AG Chart Service</title>
|
<title>AG Chart Service</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="sbChart"></div>
|
<div id="ztChart"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
407
src/App.tsx
407
src/App.tsx
@ -5,6 +5,7 @@ import {
|
|||||||
AllCommunityModule as AgGridAllCommunityModule,
|
AllCommunityModule as AgGridAllCommunityModule,
|
||||||
ModuleRegistry as AgGridModuleRegistry,
|
ModuleRegistry as AgGridModuleRegistry,
|
||||||
type ColDef,
|
type ColDef,
|
||||||
|
type ColGroupDef,
|
||||||
type ValueFormatterParams,
|
type ValueFormatterParams,
|
||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import type { AgCartesianChartOptions } from 'ag-charts-community';
|
import type { AgCartesianChartOptions } from 'ag-charts-community';
|
||||||
@ -120,6 +121,7 @@ const defaultTemplateFilterNode = {
|
|||||||
filterKey: 'templateLibrary',
|
filterKey: 'templateLibrary',
|
||||||
label: '默认模板',
|
label: '默认模板',
|
||||||
} as const;
|
} as const;
|
||||||
|
const overallSummaryKey = 'summary';
|
||||||
|
|
||||||
// const mockGeoLocationPayload = {
|
// const mockGeoLocationPayload = {
|
||||||
// checkStrictly: true,
|
// checkStrictly: true,
|
||||||
@ -161,11 +163,21 @@ type ApiBuildingFunctionStat = {
|
|||||||
max_value?: number | null;
|
max_value?: number | null;
|
||||||
avg_value?: number | null;
|
avg_value?: number | null;
|
||||||
median_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;
|
data_count?: number | null;
|
||||||
};
|
};
|
||||||
type ApiBuildingFunctionStatBatchItem = {
|
type ApiBuildingFunctionStatBatchItem = {
|
||||||
key?: string;
|
key?: string;
|
||||||
data?: ApiBuildingFunctionStat[];
|
data?: ApiBuildingFunctionStat[];
|
||||||
|
summary?: ApiBuildingFunctionStat | null;
|
||||||
};
|
};
|
||||||
type ChartDatum = {
|
type ChartDatum = {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
@ -173,6 +185,12 @@ type ChartDatum = {
|
|||||||
maxValue: number | null;
|
maxValue: number | null;
|
||||||
avgValue: number | null;
|
avgValue: number | null;
|
||||||
medianValue: 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;
|
dataCount: number | null;
|
||||||
};
|
};
|
||||||
type TreeNode = {
|
type TreeNode = {
|
||||||
@ -201,9 +219,17 @@ type SelectedFilterNode = {
|
|||||||
type PivotGridRow = {
|
type PivotGridRow = {
|
||||||
year: string;
|
year: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
summary: boolean;
|
||||||
|
lowValue: number | null;
|
||||||
|
centerValue: number | null;
|
||||||
|
highValue: number | null;
|
||||||
maxValue: number | null;
|
maxValue: number | null;
|
||||||
minValue: number | null;
|
minValue: number | null;
|
||||||
avgValue: number | null;
|
avgValue: number | null;
|
||||||
|
medianValue: number | null;
|
||||||
|
standardDeviation: number | null;
|
||||||
|
interquartileRange: number | null;
|
||||||
|
coefficientOfVariation: number | null;
|
||||||
dataCount: number | null;
|
dataCount: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -240,12 +266,22 @@ function formatChartValue(value: number, metricKey: MetricKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum {
|
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 {
|
return {
|
||||||
groupName: row.group_name || String(row.group_key ?? '未命名'),
|
groupName: row.group_name || String(row.group_key ?? '未命名'),
|
||||||
minValue: row.min_value ?? null,
|
minValue: row.min_value ?? null,
|
||||||
maxValue: row.max_value ?? null,
|
maxValue: row.max_value ?? null,
|
||||||
avgValue: row.avg_value ?? null,
|
avgValue,
|
||||||
medianValue: row.median_value ?? null,
|
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,
|
dataCount: row.data_count ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -686,6 +722,7 @@ function App() {
|
|||||||
const [statisticKey, setStatisticKey] = useState<StatisticKey>('avgValue');
|
const [statisticKey, setStatisticKey] = useState<StatisticKey>('avgValue');
|
||||||
const [metricKey, setMetricKey] = useState<MetricKey>('cost');
|
const [metricKey, setMetricKey] = useState<MetricKey>('cost');
|
||||||
const [chartViewKey, setChartViewKey] = useState<ChartViewKey>('trend');
|
const [chartViewKey, setChartViewKey] = useState<ChartViewKey>('trend');
|
||||||
|
const [workspaceFullscreen, setWorkspaceFullscreen] = useState(false);
|
||||||
const [statisticMenuOpen, setStatisticMenuOpen] = useState(false);
|
const [statisticMenuOpen, setStatisticMenuOpen] = useState(false);
|
||||||
const [metricMenuOpen, setMetricMenuOpen] = useState(false);
|
const [metricMenuOpen, setMetricMenuOpen] = useState(false);
|
||||||
const [activeContentKey, setActiveContentKey] = useState<ContentKey>('geoLocation');
|
const [activeContentKey, setActiveContentKey] = useState<ContentKey>('geoLocation');
|
||||||
@ -709,6 +746,7 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [selectedContentNodes, setSelectedContentNodes] = useState<SelectedContentNode[]>([]);
|
const [selectedContentNodes, setSelectedContentNodes] = useState<SelectedContentNode[]>([]);
|
||||||
const [chartDataBySelection, setChartDataBySelection] = useState<Record<string, ChartDatum[]>>({});
|
const [chartDataBySelection, setChartDataBySelection] = useState<Record<string, ChartDatum[]>>({});
|
||||||
|
const [chartSummaryBySelection, setChartSummaryBySelection] = useState<Record<string, ChartDatum>>({});
|
||||||
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);
|
||||||
@ -798,6 +836,7 @@ function App() {
|
|||||||
const currentViewShortLabel = chartViewKey === 'pivot' ? '表' : '趋';
|
const currentViewShortLabel = chartViewKey === 'pivot' ? '表' : '趋';
|
||||||
const pivotToggleActionLabel = chartViewKey === 'pivot' ? '切换到趋势图' : '切换到表格';
|
const pivotToggleActionLabel = chartViewKey === 'pivot' ? '切换到趋势图' : '切换到表格';
|
||||||
const pivotToggleTitle = `${chartViewKey === 'pivot' ? '当前表格' : '当前趋势图'},${pivotToggleActionLabel}`;
|
const pivotToggleTitle = `${chartViewKey === 'pivot' ? '当前表格' : '当前趋势图'},${pivotToggleActionLabel}`;
|
||||||
|
const fullscreenToggleLabel = workspaceFullscreen ? '退出全屏' : '全屏';
|
||||||
const activeContent = contentOptions.find((option) => option.key === activeContentKey) ?? contentOptions[0];
|
const activeContent = contentOptions.find((option) => option.key === activeContentKey) ?? contentOptions[0];
|
||||||
const activeTree = treeByContent[activeContentKey];
|
const activeTree = treeByContent[activeContentKey];
|
||||||
const activeFilter = filterOptions.find((option) => option.key === filterModalKey);
|
const activeFilter = filterOptions.find((option) => option.key === filterModalKey);
|
||||||
@ -846,15 +885,49 @@ function App() {
|
|||||||
return rows.map((datum) => ({
|
return rows.map((datum) => ({
|
||||||
year: datum.groupName,
|
year: datum.groupName,
|
||||||
name: node.label,
|
name: node.label,
|
||||||
|
summary: false,
|
||||||
|
lowValue: datum.thresholdLowValue,
|
||||||
|
centerValue: datum.thresholdCenterValue,
|
||||||
|
highValue: datum.thresholdHighValue,
|
||||||
maxValue: datum.maxValue,
|
maxValue: datum.maxValue,
|
||||||
minValue: datum.minValue,
|
minValue: datum.minValue,
|
||||||
avgValue: datum.avgValue,
|
avgValue: datum.avgValue,
|
||||||
|
medianValue: datum.medianValue,
|
||||||
|
standardDeviation: datum.standardDeviation,
|
||||||
|
interquartileRange: datum.interquartileRange,
|
||||||
|
coefficientOfVariation: datum.coefficientOfVariation,
|
||||||
dataCount: datum.dataCount,
|
dataCount: datum.dataCount,
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
[chartDataBySelection, selectedContentNodes],
|
[chartDataBySelection, selectedContentNodes],
|
||||||
);
|
);
|
||||||
const pivotGridColumnDefs = useMemo<ColDef<PivotGridRow>[]>(
|
const pivotGridPinnedBottomRowData = useMemo<PivotGridRow[]>(
|
||||||
|
() => {
|
||||||
|
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<PivotGridRow> | ColGroupDef<PivotGridRow>)[]>(
|
||||||
() => {
|
() => {
|
||||||
const valueFormatter = ({ value }: ValueFormatterParams<PivotGridRow, number | null>) => (
|
const valueFormatter = ({ value }: ValueFormatterParams<PivotGridRow, number | null>) => (
|
||||||
value == null ? '' : formatChartValue(Number(value), requestMetricKey)
|
value == null ? '' : formatChartValue(Number(value), requestMetricKey)
|
||||||
@ -874,34 +947,86 @@ function App() {
|
|||||||
minWidth: 108,
|
minWidth: 108,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'maxValue',
|
headerName: '基准阀值',
|
||||||
headerName: '最大值',
|
children: [
|
||||||
type: 'numericColumn',
|
{
|
||||||
minWidth: 78,
|
field: 'lowValue',
|
||||||
valueFormatter,
|
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: '样本统计值(括号显示样本数量)',
|
||||||
headerName: '最小值',
|
children: [
|
||||||
type: 'numericColumn',
|
{
|
||||||
minWidth: 78,
|
field: 'maxValue',
|
||||||
valueFormatter,
|
headerName: '最大值',
|
||||||
},
|
type: 'numericColumn',
|
||||||
{
|
minWidth: 78,
|
||||||
field: 'avgValue',
|
valueFormatter,
|
||||||
headerName: '平均值',
|
},
|
||||||
type: 'numericColumn',
|
{
|
||||||
minWidth: 78,
|
field: 'minValue',
|
||||||
valueFormatter,
|
headerName: '最小值',
|
||||||
},
|
type: 'numericColumn',
|
||||||
{
|
minWidth: 78,
|
||||||
field: 'dataCount',
|
valueFormatter,
|
||||||
headerName: '数量',
|
},
|
||||||
type: 'numericColumn',
|
{
|
||||||
minWidth: 70,
|
field: 'avgValue',
|
||||||
valueFormatter: ({ value }: ValueFormatterParams<PivotGridRow, number | null>) => (
|
headerName: '平均值',
|
||||||
value == null ? '' : formatChartValue(Number(value), 'dataCount')
|
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<PivotGridRow, number | null>) => (
|
||||||
|
value == null ? '' : formatNumber(Number(value), 4)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -1172,6 +1297,10 @@ function App() {
|
|||||||
const { [selectionKey]: _removed, ...rest } = data;
|
const { [selectionKey]: _removed, ...rest } = data;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
setChartSummaryBySelection((data) => {
|
||||||
|
const { [selectionKey]: _removed, ...rest } = data;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
return current.filter((_, index) => index !== existingIndex);
|
return current.filter((_, index) => index !== existingIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1185,6 +1314,7 @@ function App() {
|
|||||||
setActiveContentKey(nextContentKey);
|
setActiveContentKey(nextContentKey);
|
||||||
setSelectedContentNodes([]);
|
setSelectedContentNodes([]);
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setChartSummaryBySelection({});
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
setLoadingHint('');
|
setLoadingHint('');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -1416,6 +1546,7 @@ function App() {
|
|||||||
resetIndicatorTreeState();
|
resetIndicatorTreeState();
|
||||||
}
|
}
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setChartSummaryBySelection({});
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
if (selectedContentNodes.length > 0) {
|
if (selectedContentNodes.length > 0) {
|
||||||
setLoadingHint('正在按筛选条件重新计算');
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
@ -1448,6 +1579,7 @@ function App() {
|
|||||||
resetIndicatorTreeState();
|
resetIndicatorTreeState();
|
||||||
}
|
}
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setChartSummaryBySelection({});
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
if (selectedContentNodes.length > 0) {
|
if (selectedContentNodes.length > 0) {
|
||||||
setLoadingHint('正在按筛选条件重新计算');
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
@ -1459,6 +1591,7 @@ function App() {
|
|||||||
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
||||||
setMetricKey(nextMetricKey);
|
setMetricKey(nextMetricKey);
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setChartSummaryBySelection({});
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
setLoadingHint('正在重新加载数据');
|
setLoadingHint('正在重新加载数据');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -1471,12 +1604,33 @@ function App() {
|
|||||||
setStatisticMenuOpen(false);
|
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') => {
|
const openGridFilterModal = (filterKey: 'templateLibrary' | 'indicatorTree') => {
|
||||||
setMetricMenuOpen(false);
|
setMetricMenuOpen(false);
|
||||||
setStatisticMenuOpen(false);
|
setStatisticMenuOpen(false);
|
||||||
openFilterModal(filterKey);
|
openFilterModal(filterKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
setWorkspaceFullscreen(document.fullscreenElement === workspaceRef.current);
|
||||||
|
};
|
||||||
|
handleFullscreenChange();
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!contentTreeConfigs[activeContentKey]) return;
|
if (!contentTreeConfigs[activeContentKey]) return;
|
||||||
if (treeByContent[activeContentKey].length > 0 || treeInitialLoadStartedRef.current[activeContentKey]) return;
|
if (treeByContent[activeContentKey].length > 0 || treeInitialLoadStartedRef.current[activeContentKey]) return;
|
||||||
@ -1515,10 +1669,6 @@ function App() {
|
|||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
if (selectedContentNodes.length === 0) {
|
if (selectedContentNodes.length === 0) {
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
setLoading(false);
|
|
||||||
setLoadingHint('');
|
|
||||||
setLoadError(null);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -1526,7 +1676,7 @@ function App() {
|
|||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
try {
|
try {
|
||||||
const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
|
||||||
if (!hasAnyMissingNode) {
|
if (selectedContentNodes.length > 0 && !hasAnyMissingNode && chartSummaryBySelection[overallSummaryKey]) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setLoadingHint('');
|
setLoadingHint('');
|
||||||
return;
|
return;
|
||||||
@ -1557,10 +1707,19 @@ function App() {
|
|||||||
const results = (payload.data ?? [])
|
const results = (payload.data ?? [])
|
||||||
.filter((item) => item.key)
|
.filter((item) => item.key)
|
||||||
.map((item) => [item.key as string, (item.data ?? []).map(normalizeStat).slice(0, 36)] as const);
|
.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) => ({
|
setChartDataBySelection((current) => ({
|
||||||
...current,
|
...current,
|
||||||
...Object.fromEntries(results),
|
...Object.fromEntries(results),
|
||||||
}));
|
}));
|
||||||
|
setChartSummaryBySelection((current) => ({
|
||||||
|
...current,
|
||||||
|
...Object.fromEntries(summaries),
|
||||||
|
...(overallSummary ? { [overallSummaryKey]: normalizeStat(overallSummary) } : {}),
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
setLoadError(error instanceof Error ? error.message : '接口请求失败');
|
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) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key !== 'F11') return;
|
if (event.key !== 'F11') return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleFullscreen();
|
toggleWorkspaceFullscreen();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
@ -1689,7 +1840,7 @@ function App() {
|
|||||||
} else if (button.classList.contains('chart-pivot-button')) {
|
} else if (button.classList.contains('chart-pivot-button')) {
|
||||||
togglePivotView();
|
togglePivotView();
|
||||||
} else {
|
} else {
|
||||||
toggleFullscreen();
|
toggleWorkspaceFullscreen();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1762,6 +1913,7 @@ function App() {
|
|||||||
selectedContentNodes.length,
|
selectedContentNodes.length,
|
||||||
statisticMenuOpen,
|
statisticMenuOpen,
|
||||||
togglePivotView,
|
togglePivotView,
|
||||||
|
toggleWorkspaceFullscreen,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const chartOptions = useMemo<AgCartesianChartOptions>(() => {
|
const chartOptions = useMemo<AgCartesianChartOptions>(() => {
|
||||||
@ -2073,6 +2225,7 @@ function App() {
|
|||||||
planningForm: [],
|
planningForm: [],
|
||||||
});
|
});
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
|
setChartSummaryBySelection({});
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
if (selectedContentNodes.length > 0) {
|
if (selectedContentNodes.length > 0) {
|
||||||
setLoadingHint('正在按筛选条件重新计算');
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
@ -2138,9 +2291,23 @@ function App() {
|
|||||||
>
|
>
|
||||||
{currentViewShortLabel}
|
{currentViewShortLabel}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="chart-grid-tool-button chart-grid-tool-button--fullscreen"
|
||||||
|
type="button"
|
||||||
|
title={fullscreenToggleLabel}
|
||||||
|
aria-label={fullscreenToggleLabel}
|
||||||
|
aria-pressed={workspaceFullscreen}
|
||||||
|
onClick={toggleWorkspaceFullscreen}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`anticon ${workspaceFullscreen ? 'anticon-shrink' : 'anticon-arrow-salt'} ag-charts-myButton-fullScreen ag-charts-diy-button`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
{renderMetricSwitcher('grid')}
|
{renderMetricSwitcher('grid')}
|
||||||
<AgGridReact<PivotGridRow>
|
<AgGridReact<PivotGridRow>
|
||||||
rowData={pivotGridRowData}
|
rowData={pivotGridRowData}
|
||||||
|
pinnedBottomRowData={pivotGridPinnedBottomRowData}
|
||||||
columnDefs={pivotGridColumnDefs}
|
columnDefs={pivotGridColumnDefs}
|
||||||
containerStyle={{ width: '100%', height: '100%' }}
|
containerStyle={{ width: '100%', height: '100%' }}
|
||||||
theme="legacy"
|
theme="legacy"
|
||||||
@ -2190,83 +2357,83 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
{filterModalKey && activeFilter ? (
|
||||||
{filterModalKey && activeFilter ? (
|
<div className="filter-modal-backdrop" role="presentation" onMouseDown={closeFilterModal}>
|
||||||
<div className="filter-modal-backdrop" role="presentation" onMouseDown={closeFilterModal}>
|
<section
|
||||||
<section
|
className="filter-modal"
|
||||||
className="filter-modal"
|
role="dialog"
|
||||||
role="dialog"
|
aria-modal="true"
|
||||||
aria-modal="true"
|
aria-label={`${activeFilter.label}筛选`}
|
||||||
aria-label={`${activeFilter.label}筛选`}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
>
|
||||||
>
|
<header className="filter-modal-header">
|
||||||
<header className="filter-modal-header">
|
<h2>{activeFilter.label}</h2>
|
||||||
<h2>{activeFilter.label}</h2>
|
<button className="filter-modal-close" type="button" aria-label="关闭" onClick={closeFilterModal}>×</button>
|
||||||
<button className="filter-modal-close" type="button" aria-label="关闭" onClick={closeFilterModal}>×</button>
|
</header>
|
||||||
</header>
|
<div className="filter-modal-search">
|
||||||
<div className="filter-modal-search">
|
<input
|
||||||
<input
|
type="search"
|
||||||
type="search"
|
value={filterSearchValue}
|
||||||
value={filterSearchValue}
|
placeholder="搜索"
|
||||||
placeholder="搜索"
|
onChange={(event) => {
|
||||||
onChange={(event) => {
|
const nextValue = event.target.value;
|
||||||
const nextValue = event.target.value;
|
setFilterSearchValue(nextValue);
|
||||||
setFilterSearchValue(nextValue);
|
if (!filterSearchComposingRef.current && filterModalKey) {
|
||||||
if (!filterSearchComposingRef.current && filterModalKey) {
|
scheduleFilterSearch(filterModalKey, nextValue);
|
||||||
scheduleFilterSearch(filterModalKey, nextValue);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onCompositionStart={() => {
|
||||||
onCompositionStart={() => {
|
filterSearchComposingRef.current = true;
|
||||||
filterSearchComposingRef.current = true;
|
}}
|
||||||
}}
|
onCompositionEnd={(event) => {
|
||||||
onCompositionEnd={(event) => {
|
filterSearchComposingRef.current = false;
|
||||||
filterSearchComposingRef.current = false;
|
const nextValue = event.currentTarget.value;
|
||||||
const nextValue = event.currentTarget.value;
|
setFilterSearchValue(nextValue);
|
||||||
setFilterSearchValue(nextValue);
|
if (filterModalKey) {
|
||||||
if (filterModalKey) {
|
scheduleFilterSearch(filterModalKey, nextValue);
|
||||||
scheduleFilterSearch(filterModalKey, nextValue);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="filter-modal-selected">
|
||||||
<div className="filter-modal-selected">
|
{draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'}
|
||||||
{draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'}
|
</div>
|
||||||
</div>
|
<div className="filter-modal-tree">
|
||||||
<div className="filter-modal-tree">
|
{activeFilterTreeLoading ? (
|
||||||
{activeFilterTreeLoading ? (
|
<div className="content-tree-empty">加载中</div>
|
||||||
<div className="content-tree-empty">加载中</div>
|
) : activeFilterTreeError ? (
|
||||||
) : activeFilterTreeError ? (
|
<div className="content-tree-empty">{activeFilterTreeError}</div>
|
||||||
<div className="content-tree-empty">{activeFilterTreeError}</div>
|
) : activeFilterDisplayTree.length > 0 ? (
|
||||||
) : activeFilterDisplayTree.length > 0 ? (
|
renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode)
|
||||||
renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode)
|
) : (
|
||||||
) : (
|
<div className="content-tree-empty">{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}</div>
|
||||||
<div className="content-tree-empty">{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<footer className="filter-modal-actions">
|
|
||||||
<button
|
|
||||||
className="filter-modal-clear"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setDraftFilterNodes(
|
|
||||||
isTemplateFilterKey(filterModalKey)
|
|
||||||
? getDefaultTemplateFilterNodes()
|
|
||||||
: isIndicatorTreeFilterKey(filterModalKey)
|
|
||||||
? defaultIndicatorTreeNodes
|
|
||||||
: [],
|
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
清空当前
|
<footer className="filter-modal-actions">
|
||||||
</button>
|
<button
|
||||||
<button className="filter-modal-cancel" type="button" onClick={closeFilterModal}>
|
className="filter-modal-clear"
|
||||||
取消
|
type="button"
|
||||||
</button>
|
onClick={() => setDraftFilterNodes(
|
||||||
<button className="filter-modal-confirm" type="button" onClick={applyFilterModal}>
|
isTemplateFilterKey(filterModalKey)
|
||||||
确认
|
? getDefaultTemplateFilterNodes()
|
||||||
</button>
|
: isIndicatorTreeFilterKey(filterModalKey)
|
||||||
</footer>
|
? defaultIndicatorTreeNodes
|
||||||
</section>
|
: [],
|
||||||
</div>
|
)}
|
||||||
) : null}
|
>
|
||||||
|
清空当前
|
||||||
|
</button>
|
||||||
|
<button className="filter-modal-cancel" type="button" onClick={closeFilterModal}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button className="filter-modal-confirm" type="button" onClick={applyFilterModal}>
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartContainerId = 'sbChart';
|
const chartContainerId = 'ztChart';
|
||||||
|
|
||||||
const getRootRegistry = () => {
|
const getRootRegistry = () => {
|
||||||
window.__chartReactRoots ??= new WeakMap<Element, ReturnType<typeof createRoot>>();
|
window.__chartReactRoots ??= new WeakMap<Element, ReturnType<typeof createRoot>>();
|
||||||
|
|||||||
@ -471,6 +471,10 @@ button {
|
|||||||
top: 80px;
|
top: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-grid-tool-button--fullscreen {
|
||||||
|
top: 114px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-grid-tool-button:hover {
|
.chart-grid-tool-button:hover {
|
||||||
color: #0078a8;
|
color: #0078a8;
|
||||||
border-color: rgba(0, 120, 168, 0.36);
|
border-color: rgba(0, 120, 168, 0.36);
|
||||||
@ -501,7 +505,7 @@ button {
|
|||||||
|
|
||||||
.metric-switcher--grid {
|
.metric-switcher--grid {
|
||||||
left: 24px;
|
left: 24px;
|
||||||
top: 114px;
|
top: 148px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-switcher-button {
|
.metric-switcher-button {
|
||||||
@ -661,15 +665,19 @@ button {
|
|||||||
box-shadow: 0 1px 5px rgba(69, 54, 36, 0.12);
|
box-shadow: 0 1px 5px rgba(69, 54, 36, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .ag-charts-myButton-fullScreen {
|
.chart-frame .ag-charts-myButton-fullScreen,
|
||||||
|
.chart-grid-tool-button .ag-charts-myButton-fullScreen {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
margin: 4px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .ag-charts-myButton-fullScreen::before,
|
.chart-frame .ag-charts-myButton-fullScreen::before,
|
||||||
.chart-frame .ag-charts-myButton-fullScreen::after {
|
.chart-frame .ag-charts-myButton-fullScreen::after,
|
||||||
|
.chart-grid-tool-button .ag-charts-myButton-fullScreen::before,
|
||||||
|
.chart-grid-tool-button .ag-charts-myButton-fullScreen::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: block;
|
display: block;
|
||||||
@ -678,19 +686,22 @@ button {
|
|||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .anticon-arrow-salt::before {
|
.chart-frame .anticon-arrow-salt::before,
|
||||||
|
.chart-grid-tool-button .anticon-arrow-salt::before {
|
||||||
border-top: 1px solid currentColor;
|
border-top: 1px solid currentColor;
|
||||||
border-right: 1px solid currentColor;
|
border-right: 1px solid currentColor;
|
||||||
clip-path: polygon(54% 0, 100% 0, 100% 46%, 86% 46%, 86% 14%, 54% 14%);
|
clip-path: polygon(54% 0, 100% 0, 100% 46%, 86% 46%, 86% 14%, 54% 14%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .anticon-arrow-salt::after {
|
.chart-frame .anticon-arrow-salt::after,
|
||||||
|
.chart-grid-tool-button .anticon-arrow-salt::after {
|
||||||
border-bottom: 1px solid currentColor;
|
border-bottom: 1px solid currentColor;
|
||||||
border-left: 1px solid currentColor;
|
border-left: 1px solid currentColor;
|
||||||
clip-path: polygon(0 54%, 14% 54%, 14% 86%, 46% 86%, 46% 100%, 0 100%);
|
clip-path: polygon(0 54%, 14% 54%, 14% 86%, 46% 86%, 46% 100%, 0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .anticon-shrink::before {
|
.chart-frame .anticon-shrink::before,
|
||||||
|
.chart-grid-tool-button .anticon-shrink::before {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@ -699,7 +710,8 @@ button {
|
|||||||
border-bottom: 1px solid currentColor;
|
border-bottom: 1px solid currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-frame .anticon-shrink::after {
|
.chart-frame .anticon-shrink::after,
|
||||||
|
.chart-grid-tool-button .anticon-shrink::after {
|
||||||
right: 2px;
|
right: 2px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
left: auto;
|
left: auto;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user