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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sbChart"></div>
|
||||
<div id="ztChart"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
407
src/App.tsx
407
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<StatisticKey>('avgValue');
|
||||
const [metricKey, setMetricKey] = useState<MetricKey>('cost');
|
||||
const [chartViewKey, setChartViewKey] = useState<ChartViewKey>('trend');
|
||||
const [workspaceFullscreen, setWorkspaceFullscreen] = useState(false);
|
||||
const [statisticMenuOpen, setStatisticMenuOpen] = useState(false);
|
||||
const [metricMenuOpen, setMetricMenuOpen] = useState(false);
|
||||
const [activeContentKey, setActiveContentKey] = useState<ContentKey>('geoLocation');
|
||||
@ -709,6 +746,7 @@ function App() {
|
||||
});
|
||||
const [selectedContentNodes, setSelectedContentNodes] = useState<SelectedContentNode[]>([]);
|
||||
const [chartDataBySelection, setChartDataBySelection] = useState<Record<string, ChartDatum[]>>({});
|
||||
const [chartSummaryBySelection, setChartSummaryBySelection] = useState<Record<string, ChartDatum>>({});
|
||||
const [chartQueryVersion, setChartQueryVersion] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(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<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>) => (
|
||||
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<PivotGridRow, number | null>) => (
|
||||
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<PivotGridRow, number | null>) => (
|
||||
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<AgCartesianChartOptions>(() => {
|
||||
@ -2073,6 +2225,7 @@ function App() {
|
||||
planningForm: [],
|
||||
});
|
||||
setChartDataBySelection({});
|
||||
setChartSummaryBySelection({});
|
||||
setLoadError(null);
|
||||
if (selectedContentNodes.length > 0) {
|
||||
setLoadingHint('正在按筛选条件重新计算');
|
||||
@ -2138,9 +2291,23 @@ function App() {
|
||||
>
|
||||
{currentViewShortLabel}
|
||||
</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')}
|
||||
<AgGridReact<PivotGridRow>
|
||||
rowData={pivotGridRowData}
|
||||
pinnedBottomRowData={pivotGridPinnedBottomRowData}
|
||||
columnDefs={pivotGridColumnDefs}
|
||||
containerStyle={{ width: '100%', height: '100%' }}
|
||||
theme="legacy"
|
||||
@ -2190,83 +2357,83 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
{filterModalKey && activeFilter ? (
|
||||
<div className="filter-modal-backdrop" role="presentation" onMouseDown={closeFilterModal}>
|
||||
<section
|
||||
className="filter-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`${activeFilter.label}筛选`}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="filter-modal-header">
|
||||
<h2>{activeFilter.label}</h2>
|
||||
<button className="filter-modal-close" type="button" aria-label="关闭" onClick={closeFilterModal}>×</button>
|
||||
</header>
|
||||
<div className="filter-modal-search">
|
||||
<input
|
||||
type="search"
|
||||
value={filterSearchValue}
|
||||
placeholder="搜索"
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
setFilterSearchValue(nextValue);
|
||||
if (!filterSearchComposingRef.current && filterModalKey) {
|
||||
scheduleFilterSearch(filterModalKey, nextValue);
|
||||
}
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
filterSearchComposingRef.current = true;
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
filterSearchComposingRef.current = false;
|
||||
const nextValue = event.currentTarget.value;
|
||||
setFilterSearchValue(nextValue);
|
||||
if (filterModalKey) {
|
||||
scheduleFilterSearch(filterModalKey, nextValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-modal-selected">
|
||||
{draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'}
|
||||
</div>
|
||||
<div className="filter-modal-tree">
|
||||
{activeFilterTreeLoading ? (
|
||||
<div className="content-tree-empty">加载中</div>
|
||||
) : activeFilterTreeError ? (
|
||||
<div className="content-tree-empty">{activeFilterTreeError}</div>
|
||||
) : activeFilterDisplayTree.length > 0 ? (
|
||||
renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode)
|
||||
) : (
|
||||
<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
|
||||
: [],
|
||||
{filterModalKey && activeFilter ? (
|
||||
<div className="filter-modal-backdrop" role="presentation" onMouseDown={closeFilterModal}>
|
||||
<section
|
||||
className="filter-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`${activeFilter.label}筛选`}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="filter-modal-header">
|
||||
<h2>{activeFilter.label}</h2>
|
||||
<button className="filter-modal-close" type="button" aria-label="关闭" onClick={closeFilterModal}>×</button>
|
||||
</header>
|
||||
<div className="filter-modal-search">
|
||||
<input
|
||||
type="search"
|
||||
value={filterSearchValue}
|
||||
placeholder="搜索"
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
setFilterSearchValue(nextValue);
|
||||
if (!filterSearchComposingRef.current && filterModalKey) {
|
||||
scheduleFilterSearch(filterModalKey, nextValue);
|
||||
}
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
filterSearchComposingRef.current = true;
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
filterSearchComposingRef.current = false;
|
||||
const nextValue = event.currentTarget.value;
|
||||
setFilterSearchValue(nextValue);
|
||||
if (filterModalKey) {
|
||||
scheduleFilterSearch(filterModalKey, nextValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-modal-selected">
|
||||
{draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'}
|
||||
</div>
|
||||
<div className="filter-modal-tree">
|
||||
{activeFilterTreeLoading ? (
|
||||
<div className="content-tree-empty">加载中</div>
|
||||
) : activeFilterTreeError ? (
|
||||
<div className="content-tree-empty">{activeFilterTreeError}</div>
|
||||
) : activeFilterDisplayTree.length > 0 ? (
|
||||
renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode)
|
||||
) : (
|
||||
<div className="content-tree-empty">{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}</div>
|
||||
)}
|
||||
>
|
||||
清空当前
|
||||
</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}
|
||||
</div>
|
||||
<footer className="filter-modal-actions">
|
||||
<button
|
||||
className="filter-modal-clear"
|
||||
type="button"
|
||||
onClick={() => setDraftFilterNodes(
|
||||
isTemplateFilterKey(filterModalKey)
|
||||
? getDefaultTemplateFilterNodes()
|
||||
: isIndicatorTreeFilterKey(filterModalKey)
|
||||
? defaultIndicatorTreeNodes
|
||||
: [],
|
||||
)}
|
||||
>
|
||||
清空当前
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const chartContainerId = 'sbChart';
|
||||
const chartContainerId = 'ztChart';
|
||||
|
||||
const getRootRegistry = () => {
|
||||
window.__chartReactRoots ??= new WeakMap<Element, ReturnType<typeof createRoot>>();
|
||||
|
||||
@ -471,6 +471,10 @@ button {
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.chart-grid-tool-button--fullscreen {
|
||||
top: 114px;
|
||||
}
|
||||
|
||||
.chart-grid-tool-button:hover {
|
||||
color: #0078a8;
|
||||
border-color: rgba(0, 120, 168, 0.36);
|
||||
@ -501,7 +505,7 @@ button {
|
||||
|
||||
.metric-switcher--grid {
|
||||
left: 24px;
|
||||
top: 114px;
|
||||
top: 148px;
|
||||
}
|
||||
|
||||
.metric-switcher-button {
|
||||
@ -661,15 +665,19 @@ button {
|
||||
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;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 4px auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
inset: 0;
|
||||
display: block;
|
||||
@ -678,19 +686,22 @@ button {
|
||||
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-right: 1px solid currentColor;
|
||||
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-left: 1px solid currentColor;
|
||||
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;
|
||||
left: 2px;
|
||||
width: 6px;
|
||||
@ -699,7 +710,8 @@ button {
|
||||
border-bottom: 1px solid currentColor;
|
||||
}
|
||||
|
||||
.chart-frame .anticon-shrink::after {
|
||||
.chart-frame .anticon-shrink::after,
|
||||
.chart-grid-tool-button .anticon-shrink::after {
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
left: auto;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user