Compare commits

..

5 Commits

Author SHA1 Message Date
178a3e656f '1' 2026-06-01 14:48:17 +08:00
cfb6afcdce '1' 2026-05-21 09:24:15 +08:00
58abe588aa 1 2026-05-13 16:23:55 +08:00
46325ee801 Merge remote-tracking branch 'origin/main' into building-facility-object 2026-05-13 10:25:08 +08:00
c65235f377 feat: switch to building facility object 2026-05-11 10:30:14 +08:00
16 changed files with 283 additions and 241 deletions

View File

@ -0,0 +1,3 @@
[ 6918ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=ae174d51:20102
[ 7315ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:5173/favicon.ico:0
[ 331036ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=ae174d51:20102

View File

@ -1 +1,6 @@
[ 550538ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=ae174d51:20102 [ 300ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=ae174d51:20102
[ 1230ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:5173/favicon.ico:0
[ 40506ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://nest.zwgczx.com/api/v1/zw/getBuildingFunctionCostFilterTree?key=templateLibrary&currenttime=1778660578349&__random__=1778660578349:0
[ 40812ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://nest.zwgczx.com/api/v1/zw/getBuildingFunctionCostFilterTree?key=indicatorTree&templateId=3&currenttime=1778660578682&__random__=1778660578682:0
[ 52231ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://nest.zwgczx.com/api/v1/zw/getBuildingFunctionCostFilterTree?key=templateLibrary&currenttime=1778660590101&__random__=1778660590101:0
[ 53824ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://nest.zwgczx.com/api/v1/zw/getBuildingFunctionCostFilterTree?key=indicatorTree&templateId=3&currenttime=1778660591693&__random__=1778660591693:0

View File

@ -1,2 +0,0 @@
[ 136ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=ae174d51:20102
[ 595ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:5173/favicon.ico:0

View File

@ -1,2 +0,0 @@
[ 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

View File

@ -0,0 +1,84 @@
- 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=e27]: 建筑功能
- button "建设阶段" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- generic [ref=e34]: 建设阶段
- button "规划形式" [ref=e35] [cursor=pointer]:
- img [ref=e36]
- generic [ref=e41]: 规划形式
- region "年度总费用图表" [ref=e42]:
- generic [ref=e43]:
- button "纵坐标:造价(元)" [ref=e45] [cursor=pointer]: 造价(元)
- generic [ref=e46]:
- figure "图表共有0个系列":
- generic [ref=e47]:
- img "interactive chart":
- generic:
- img
- img
- region [ref=e48]
- toolbar "标注" [ref=e49]:
- button "库" [ref=e50] [cursor=pointer]:
- generic:
- generic:
- button "树" [ref=e51] [cursor=pointer]:
- generic:
- generic:
- button "均" [ref=e52] [cursor=pointer]:
- generic:
- button "Line Tool" [disabled] [ref=e53]
- button "Text Tool" [disabled] [ref=e54]
- button "Shape Tool" [disabled] [ref=e55]
- button "Fibonacci Tool" [disabled] [ref=e56]
- button "全屏(F11)" [ref=e57] [cursor=pointer]
- button "Clear annotations" [disabled] [ref=e58]
- status:
- generic: 请选择右侧分类项
- toolbar "缩放" [ref=e59]:
- button "缩小" [disabled] [ref=e60]
- button "放大" [ref=e61] [cursor=pointer]
- button "左移" [disabled] [ref=e62]
- button "右移" [disabled] [ref=e63]
- button "重置" [disabled] [ref=e64]
- complementary "选择内容" [ref=e65]:
- tablist "选择内容切换项" [ref=e66]:
- tab "自然地理区位" [selected] [ref=e67] [cursor=pointer]
- tab "设施类别" [ref=e68] [cursor=pointer]
- tab "建筑功能" [ref=e69] [cursor=pointer]
- tab "建设阶段" [ref=e70] [cursor=pointer]
- tab "规划形式" [ref=e71] [cursor=pointer]
- generic [ref=e72]:
- generic [ref=e73]: 自然地理区位
- generic [ref=e74]: 加载中

View File

@ -0,0 +1,84 @@
- 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=e27]: 建筑功能
- button "建设阶段" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- generic [ref=e34]: 建设阶段
- button "规划形式" [ref=e35] [cursor=pointer]:
- img [ref=e36]
- generic [ref=e41]: 规划形式
- region "年度总费用图表" [ref=e42]:
- generic [ref=e43]:
- button "纵坐标:造价(元)" [ref=e45] [cursor=pointer]: 造价(元)
- generic [ref=e46]:
- figure "图表共有0个系列":
- generic [ref=e47]:
- img "interactive chart":
- generic:
- img
- img
- region [ref=e48]
- toolbar "标注" [ref=e49]:
- button "库" [ref=e50] [cursor=pointer]:
- generic:
- generic:
- button "树" [ref=e51] [cursor=pointer]:
- generic:
- generic:
- button "均" [ref=e52] [cursor=pointer]:
- generic:
- button "Line Tool" [disabled] [ref=e53]
- button "Text Tool" [disabled] [ref=e54]
- button "Shape Tool" [disabled] [ref=e55]
- button "Fibonacci Tool" [disabled] [ref=e56]
- button "全屏(F11)" [ref=e57] [cursor=pointer]
- button "Clear annotations" [disabled] [ref=e58]
- status:
- generic: 请选择右侧分类项
- toolbar "缩放" [ref=e59]:
- button "缩小" [disabled] [ref=e60]
- button "放大" [ref=e61] [cursor=pointer]
- button "左移" [disabled] [ref=e62]
- button "右移" [disabled] [ref=e63]
- button "重置" [disabled] [ref=e64]
- complementary "选择内容" [ref=e65]:
- tablist "选择内容切换项" [ref=e66]:
- tab "自然地理区位" [selected] [ref=e67] [cursor=pointer]
- tab "设施类别" [ref=e68] [cursor=pointer]
- tab "建筑功能" [ref=e69] [cursor=pointer]
- tab "建设阶段" [ref=e70] [cursor=pointer]
- tab "规划形式" [ref=e71] [cursor=pointer]
- generic [ref=e72]:
- generic [ref=e73]: 自然地理区位
- generic [ref=e74]: 加载中

View File

@ -1,80 +0,0 @@
- 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 "库" [ref=e45] [cursor=pointer]:
- generic:
- generic:
- button "树" [ref=e46] [cursor=pointer]:
- generic:
- generic:
- button "均" [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)" [ref=e52] [cursor=pointer]
- button "Clear annotations" [disabled] [ref=e53]
- status:
- generic: 请选择右侧分类项
- toolbar "缩放" [ref=e54]:
- button "缩小" [disabled] [ref=e55]
- button "放大" [ref=e56] [cursor=pointer]
- button "左移" [disabled] [ref=e57]
- button "右移" [disabled] [ref=e58]
- button "重置" [disabled] [ref=e59]
- complementary "选择内容" [ref=e60]:
- tablist "选择内容切换项" [ref=e61]:
- tab "自然地理区位" [selected] [ref=e62] [cursor=pointer]
- tab "设施类别" [ref=e63] [cursor=pointer]
- tab "建设阶段" [ref=e64] [cursor=pointer]
- tab "规划形式" [ref=e65] [cursor=pointer]
- generic [ref=e66]:
- generic [ref=e67]: 自然地理区位
- generic [ref=e68]: 加载中

View File

@ -1,83 +0,0 @@
- 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

View File

@ -1,10 +1,10 @@
# 组团趋势图 # 建筑设施对象趋势图
这是一个基于 `React + AG Charts`组团趋势图页面,用来查看不同分类维度下的年度统计趋势,并通过右侧筛选条件联动图表结果。 这是一个基于 `React + AG Charts`建筑设施对象趋势图页面,用来查看不同分类维度下的年度统计趋势,并通过右侧筛选条件联动图表结果。
## 功能 ## 功能
- 左侧展示组团趋势图,支持最低值、最高值、平均值、中位数和数据量切换 - 左侧展示建筑设施对象趋势图,支持最低值、最高值、平均值、中位数和数据量切换
- 右侧展示分类树,支持按 `自然地理区位 / 设施类别 / 建设阶段 / 规划形式` 选择节点 - 右侧展示分类树,支持按 `自然地理区位 / 设施类别 / 建设阶段 / 规划形式` 选择节点
- 筛选条件支持 `省市区`,后端会根据 `uf_xzqy` 展开到区级 `id` 后再过滤 - 筛选条件支持 `省市区`,后端会根据 `uf_xzqy` 展开到区级 `id` 后再过滤
- 支持搜索筛选树节点,支持多条件叠加 - 支持搜索筛选树节点,支持多条件叠加

View File

@ -6,7 +6,7 @@
<title>AG Chart Service</title> <title>AG Chart Service</title>
</head> </head>
<body> <body>
<div id="ztChart"></div> <div id="sbChart"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@ -12,7 +12,7 @@ import {
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { AgCartesianChartOptions } from 'ag-charts-community'; import type { AgCartesianChartOptions } from 'ag-charts-community';
import { ModuleRegistry } from 'ag-charts-community'; import { ModuleRegistry } from 'ag-charts-community';
import { Building2, Construction, LayoutGrid, Library, LocateFixed, MapPinned, PanelRightClose, PanelRightOpen, Waypoints } from 'lucide-react'; import { Building2, Construction, LayoutGrid, Library, LocateFixed, MapPinned, PanelRightClose, PanelRightOpen, SquareFunction, Waypoints } from 'lucide-react';
import { import {
AnnotationsModule, AnnotationsModule,
ContextMenuModule, ContextMenuModule,
@ -29,6 +29,12 @@ AgGridModuleRegistry.registerModules([AgGridAllCommunityModule]);
const API_BASE_URL = 'https://nest.zwgczx.com/api/v1'; const API_BASE_URL = 'https://nest.zwgczx.com/api/v1';
// const API_BASE_URL = 'http://127.0.0.1:9089/api/v1'; // const API_BASE_URL = 'http://127.0.0.1:9089/api/v1';
const API_ROUTES = {
filterTree: '/zw/getBuildingFacilityObjectFilterTree',
filterTreeSearch: '/zw/getBuildingFacilityObjectFilterTreeSearch',
statsBatch: '/zw/getBuildingFacilityObjectStatsBatch',
} as const;
const statisticOptions = [ const statisticOptions = [
{ key: 'minValue', label: '最低值', shortLabel: '低' }, { key: 'minValue', label: '最低值', shortLabel: '低' },
{ key: 'maxValue', label: '最高值', shortLabel: '高' }, { key: 'maxValue', label: '最高值', shortLabel: '高' },
@ -55,6 +61,7 @@ const metricShortLabels: Record<MetricKey, string> = {
const contentOptions = [ const contentOptions = [
{ key: 'geoLocation', label: '自然地理区位' }, { key: 'geoLocation', label: '自然地理区位' },
{ key: 'facilityType', label: '设施类别' }, { key: 'facilityType', label: '设施类别' },
{ key: 'buildingFunction', label: '建筑功能' },
{ key: 'constructionStage', label: '建设阶段' }, { key: 'constructionStage', label: '建设阶段' },
{ key: 'planningForm', label: '规划形式' }, { key: 'planningForm', label: '规划形式' },
] as const; ] as const;
@ -65,6 +72,7 @@ const filterOptions = [
{ key: 'region', label: '省市区', icon: MapPinned }, { key: 'region', label: '省市区', icon: MapPinned },
{ key: 'geoLocation', label: '自然地理区位', icon: LocateFixed }, { key: 'geoLocation', label: '自然地理区位', icon: LocateFixed },
{ key: 'facilityType', label: '设施类别', icon: Building2 }, { key: 'facilityType', label: '设施类别', icon: Building2 },
{ key: 'buildingFunction', label: '建筑功能', icon: SquareFunction },
{ key: 'constructionStage', label: '建设阶段', icon: Construction }, { key: 'constructionStage', label: '建设阶段', icon: Construction },
{ key: 'planningForm', label: '规划形式', icon: LayoutGrid }, { key: 'planningForm', label: '规划形式', icon: LayoutGrid },
] as const; ] as const;
@ -103,6 +111,17 @@ const contentTreeConfigs = {
fieldid: '305426', fieldid: '305426',
defaultExpandedLevel: 3, defaultExpandedLevel: 3,
}, },
buildingFunction: {
endpoint: '/api/public/browser/data/256',
treeid: '95004',
fieldid: '305919',
defaultExpandedLevel: 0,
browserParams: {
workflowid: '182032',
wfid: '182032',
billid: '-1817',
},
},
constructionStage: { constructionStage: {
endpoint: '/api/public/browser/data/256', endpoint: '/api/public/browser/data/256',
treeid: '94007', treeid: '94007',
@ -157,6 +176,7 @@ type StatisticKey = (typeof statisticOptions)[number]['key'];
type MetricKey = (typeof metricOptions)[number]['key']; type MetricKey = (typeof metricOptions)[number]['key'];
type ContentKey = (typeof contentOptions)[number]['key']; type ContentKey = (typeof contentOptions)[number]['key'];
type FilterKey = (typeof filterOptions)[number]['key']; type FilterKey = (typeof filterOptions)[number]['key'];
type GroupKey = 'year';
type ChartViewKey = 'trend' | 'pivot'; type ChartViewKey = 'trend' | 'pivot';
type ApiBuildingFunctionStat = { type ApiBuildingFunctionStat = {
group_key?: string | number | null; group_key?: string | number | null;
@ -568,10 +588,6 @@ function isDefaultTemplateSelection(nodes: SelectedFilterNode[]) {
return nodes.length === 1 && nodes[0]?.id === defaultTemplateFilterNode.id; return nodes.length === 1 && nodes[0]?.id === defaultTemplateFilterNode.id;
} }
function isSameFilterSelection(a: SelectedFilterNode[], b: SelectedFilterNode[]) {
return a.length === b.length && a.every((node, index) => node.id === b[index]?.id);
}
function getDefaultIndicatorTreeFilterNodes(nodes: TreeNode[]): SelectedFilterNode[] { function getDefaultIndicatorTreeFilterNodes(nodes: TreeNode[]): SelectedFilterNode[] {
const defaultNode = nodes[0]; const defaultNode = nodes[0];
if (!defaultNode) return []; if (!defaultNode) return [];
@ -710,6 +726,7 @@ function App() {
const treeInitialLoadStartedRef = useRef<Record<ContentKey, boolean>>({ const treeInitialLoadStartedRef = useRef<Record<ContentKey, boolean>>({
geoLocation: false, geoLocation: false,
facilityType: false, facilityType: false,
buildingFunction: false,
constructionStage: false, constructionStage: false,
planningForm: false, planningForm: false,
}); });
@ -719,11 +736,13 @@ function App() {
region: false, region: false,
geoLocation: false, geoLocation: false,
facilityType: false, facilityType: false,
buildingFunction: false,
constructionStage: false, constructionStage: false,
planningForm: false, planningForm: false,
}); });
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 [groupKey, setGroupKey] = useState<GroupKey>('year');
const [chartViewKey, setChartViewKey] = useState<ChartViewKey>('trend'); const [chartViewKey, setChartViewKey] = useState<ChartViewKey>('trend');
const [workspaceFullscreen, setWorkspaceFullscreen] = useState(false); const [workspaceFullscreen, setWorkspaceFullscreen] = useState(false);
const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false); const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false);
@ -733,18 +752,21 @@ function App() {
const [treeByContent, setTreeByContent] = useState<Record<ContentKey, TreeNode[]>>({ const [treeByContent, setTreeByContent] = useState<Record<ContentKey, TreeNode[]>>({
geoLocation: [], geoLocation: [],
facilityType: [], facilityType: [],
buildingFunction: [],
constructionStage: [], constructionStage: [],
planningForm: [], planningForm: [],
}); });
const [treeLoadingByContent, setTreeLoadingByContent] = useState<Record<ContentKey, boolean>>({ const [treeLoadingByContent, setTreeLoadingByContent] = useState<Record<ContentKey, boolean>>({
geoLocation: false, geoLocation: false,
facilityType: false, facilityType: false,
buildingFunction: false,
constructionStage: false, constructionStage: false,
planningForm: false, planningForm: false,
}); });
const [treeErrorByContent, setTreeErrorByContent] = useState<Record<ContentKey, string | null>>({ const [treeErrorByContent, setTreeErrorByContent] = useState<Record<ContentKey, string | null>>({
geoLocation: null, geoLocation: null,
facilityType: null, facilityType: null,
buildingFunction: null,
constructionStage: null, constructionStage: null,
planningForm: null, planningForm: null,
}); });
@ -761,6 +783,7 @@ function App() {
region: [], region: [],
geoLocation: [], geoLocation: [],
facilityType: [], facilityType: [],
buildingFunction: [],
constructionStage: [], constructionStage: [],
planningForm: [], planningForm: [],
}); });
@ -770,6 +793,7 @@ function App() {
region: false, region: false,
geoLocation: false, geoLocation: false,
facilityType: false, facilityType: false,
buildingFunction: false,
constructionStage: false, constructionStage: false,
planningForm: false, planningForm: false,
}); });
@ -779,6 +803,7 @@ function App() {
region: null, region: null,
geoLocation: null, geoLocation: null,
facilityType: null, facilityType: null,
buildingFunction: null,
constructionStage: null, constructionStage: null,
planningForm: null, planningForm: null,
}); });
@ -788,6 +813,7 @@ function App() {
region: [], region: [],
geoLocation: [], geoLocation: [],
facilityType: [], facilityType: [],
buildingFunction: [],
constructionStage: [], constructionStage: [],
planningForm: [], planningForm: [],
}); });
@ -797,6 +823,7 @@ function App() {
region: false, region: false,
geoLocation: false, geoLocation: false,
facilityType: false, facilityType: false,
buildingFunction: false,
constructionStage: false, constructionStage: false,
planningForm: false, planningForm: false,
}); });
@ -806,6 +833,7 @@ function App() {
region: null, region: null,
geoLocation: null, geoLocation: null,
facilityType: null, facilityType: null,
buildingFunction: null,
constructionStage: null, constructionStage: null,
planningForm: null, planningForm: null,
}); });
@ -815,6 +843,7 @@ function App() {
region: [], region: [],
geoLocation: [], geoLocation: [],
facilityType: [], facilityType: [],
buildingFunction: [],
constructionStage: [], constructionStage: [],
planningForm: [], planningForm: [],
}); });
@ -829,6 +858,7 @@ function App() {
region: 0, region: 0,
geoLocation: 0, geoLocation: 0,
facilityType: 0, facilityType: 0,
buildingFunction: 0,
constructionStage: 0, constructionStage: 0,
planningForm: 0, planningForm: 0,
}); });
@ -861,7 +891,10 @@ function App() {
); );
const indicatorSelectionLabel = appliedFilters.indicatorTree[0]?.label ?? defaultIndicatorTreeNodes[0]?.label ?? ''; const indicatorSelectionLabel = appliedFilters.indicatorTree[0]?.label ?? defaultIndicatorTreeNodes[0]?.label ?? '';
const activeFilterCount = Object.entries(appliedFilters).reduce((total, [key, nodes]) => ( const activeFilterCount = Object.entries(appliedFilters).reduce((total, [key, nodes]) => (
key === 'templateLibrary' && isDefaultTemplateSelection(nodes) ? total : total + nodes.length (key === 'templateLibrary' && isDefaultTemplateSelection(nodes))
|| (key === 'indicatorTree' && isDefaultIndicatorTreeSelection(nodes, filterTreeByKey.indicatorTree))
? total
: total + nodes.length
), 0); ), 0);
const chartEmptyText = selectedContentNodes.length === 0 const chartEmptyText = selectedContentNodes.length === 0
? '请选择右侧分类项' ? '请选择右侧分类项'
@ -1057,6 +1090,14 @@ function App() {
() => new Set(draftFilterNodes.map((node) => getFilterSelectionKey(node.filterKey, node.id))), () => new Set(draftFilterNodes.map((node) => getFilterSelectionKey(node.filterKey, node.id))),
[draftFilterNodes], [draftFilterNodes],
); );
useEffect(() => {
if (defaultIndicatorTreeNodes.length === 0) return;
setAppliedFilters((current) => {
if (current.indicatorTree.length > 0) return current;
return { ...current, indicatorTree: defaultIndicatorTreeNodes };
});
}, [defaultIndicatorTreeNodes]);
const appliedFilterPayload = useMemo( const appliedFilterPayload = useMemo(
() => filterOptions () => filterOptions
.map((option) => ({ .map((option) => ({
@ -1083,6 +1124,7 @@ function App() {
} }
const treeParams = { const treeParams = {
...browserTreeDefaults, ...browserTreeDefaults,
...('browserParams' in config ? config.browserParams : {}),
treeid: config.treeid, treeid: config.treeid,
cube_treeid: config.treeid, cube_treeid: config.treeid,
fieldid: config.fieldid, fieldid: config.fieldid,
@ -1140,7 +1182,7 @@ function App() {
}; };
const fetchRegionFilterTree = async (signal?: AbortSignal) => { const fetchRegionFilterTree = async (signal?: AbortSignal) => {
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTree?${buildQuery({ key: 'region' })}`, { const response = await fetch(`${API_BASE_URL}${API_ROUTES.filterTree}?${buildQuery({ key: 'region' })}`, {
signal, signal,
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
@ -1185,7 +1227,7 @@ function App() {
}; };
const fetchBackendFilterTreeSearch = async (filterKey: FilterKey, keyword: string, signal?: AbortSignal) => { const fetchBackendFilterTreeSearch = async (filterKey: FilterKey, keyword: string, signal?: AbortSignal) => {
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTreeSearch?${buildQuery({ const response = await fetch(`${API_BASE_URL}${API_ROUTES.filterTreeSearch}?${buildQuery({
key: filterKey, key: filterKey,
keyword, keyword,
nodePrefix: isContentFilterKey(filterKey) ? getTreeNodePrefix(filterTreeByKey[filterKey]) : '', nodePrefix: isContentFilterKey(filterKey) ? getTreeNodePrefix(filterTreeByKey[filterKey]) : '',
@ -1370,6 +1412,10 @@ function App() {
}); });
}; };
useEffect(() => {
ensureFilterTreeLoaded('indicatorTree');
}, [selectedTemplateId]);
const openFilterModal = (filterKey: FilterKey) => { const openFilterModal = (filterKey: FilterKey) => {
setFilterModalKey(filterKey); setFilterModalKey(filterKey);
if (isIndicatorTreeFilterKey(filterKey) && appliedFilters[filterKey].length === 0 && defaultIndicatorTreeNodes.length > 0) { if (isIndicatorTreeFilterKey(filterKey) && appliedFilters[filterKey].length === 0 && defaultIndicatorTreeNodes.length > 0) {
@ -1386,10 +1432,6 @@ function App() {
ensureFilterTreeLoaded(filterKey); ensureFilterTreeLoaded(filterKey);
}; };
useEffect(() => {
ensureFilterTreeLoaded('indicatorTree');
}, [selectedTemplateId]);
const closeFilterModal = () => { const closeFilterModal = () => {
if (filterModalKey) { if (filterModalKey) {
filterSearchRequestSeqRef.current[filterModalKey] += 1; filterSearchRequestSeqRef.current[filterModalKey] += 1;
@ -1544,22 +1586,18 @@ function App() {
if (nextDraftNodes.length === 0) { if (nextDraftNodes.length === 0) {
nextDraftNodes = defaultIndicatorTreeNodes; nextDraftNodes = defaultIndicatorTreeNodes;
} }
if (isDefaultIndicatorTreeSelection(nextDraftNodes, filterTreeByKey.indicatorTree)) {
nextDraftNodes = [];
}
} }
const shouldReloadIndicatorTree = isTemplateFilterKey(filterModalKey) && !isSameFilterSelection(appliedFilters.templateLibrary, nextDraftNodes);
setAppliedFilters((current) => { setAppliedFilters((current) => {
const nextFilters = { const nextFilters = {
...current, ...current,
[filterModalKey]: nextDraftNodes, [filterModalKey]: nextDraftNodes,
}; };
if (shouldReloadIndicatorTree) { if (isTemplateFilterKey(filterModalKey)) {
nextFilters.indicatorTree = []; nextFilters.indicatorTree = [];
} }
return nextFilters; return nextFilters;
}); });
if (shouldReloadIndicatorTree) { if (isTemplateFilterKey(filterModalKey)) {
resetIndicatorTreeState(); resetIndicatorTreeState();
} }
setChartDataBySelection({}); setChartDataBySelection({});
@ -1579,7 +1617,7 @@ function App() {
: isIndicatorTreeFilterKey(filterKey) : isIndicatorTreeFilterKey(filterKey)
? defaultIndicatorTreeNodes ? defaultIndicatorTreeNodes
: []; : [];
if (isSameFilterSelection(appliedFilters[filterKey], nextNodes)) { if (appliedFilters[filterKey].length === nextNodes.length && appliedFilters[filterKey].every((node, index) => node.id === nextNodes[index]?.id)) {
return; return;
} }
setAppliedFilters((current) => { setAppliedFilters((current) => {
@ -1699,14 +1737,14 @@ function App() {
return; return;
} }
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStatsBatch`, { const response = await fetch(`${API_BASE_URL}${API_ROUTES.statsBatch}`, {
method: 'POST', method: 'POST',
signal: controller.signal, signal: controller.signal,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
groupBy: 'year', groupBy: groupKey,
metric: requestMetricKey, metric: requestMetricKey,
templateId: selectedTemplateId, templateId: selectedTemplateId,
filters: appliedFilterPayload, filters: appliedFilterPayload,
@ -1753,7 +1791,7 @@ function App() {
return () => { return () => {
controller.abort(); controller.abort();
}; };
}, [appliedFilterPayload, chartQueryVersion, metricKey, requestMetricKey, selectedContentNodes, selectedTemplateId]); }, [appliedFilterPayload, chartQueryVersion, groupKey, metricKey, requestMetricKey, selectedContentNodes, selectedTemplateId]);
useEffect(() => { useEffect(() => {
const frame = chartFrameRef.current; const frame = chartFrameRef.current;
@ -1792,7 +1830,6 @@ function App() {
indicatorButton.classList.add('chart-indicator-button'); indicatorButton.classList.add('chart-indicator-button');
enableCustomToolbarButton(indicatorButton); enableCustomToolbarButton(indicatorButton);
setButtonAttribute(indicatorButton, 'aria-expanded', String(filterModalKey === 'indicatorTree')); setButtonAttribute(indicatorButton, 'aria-expanded', String(filterModalKey === 'indicatorTree'));
setButtonAttribute(indicatorButton, 'title', '指标树形');
} }
const button = getFullscreenButton(); const button = getFullscreenButton();
@ -1941,7 +1978,7 @@ function App() {
}, [chartViewKey, fitPivotGridColumns, metricKey, pivotGridRowData.length, rightPanelCollapsed]); }, [chartViewKey, fitPivotGridColumns, metricKey, pivotGridRowData.length, rightPanelCollapsed]);
const chartOptions = useMemo<AgCartesianChartOptions>(() => { const chartOptions = useMemo<AgCartesianChartOptions>(() => {
const trendData = groupNames.map((groupName) => { const visibleData = groupNames.map((groupName) => {
const row: Record<string, string | number | null> = { groupName }; const row: Record<string, string | number | null> = { groupName };
selectedContentNodes.forEach((node) => { selectedContentNodes.forEach((node) => {
const datum = chartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName); const datum = chartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName);
@ -1949,32 +1986,6 @@ function App() {
}); });
return row; return row;
}); });
const series = selectedContentNodes.map((node) => ({
type: 'line' as const,
xKey: 'groupName',
yKey: getSeriesValueKey(node.contentKey, node.id),
yName: `${node.label} ${seriesValueLabel}`,
stroke: node.color,
strokeWidth: 2,
connectMissingData: true,
marker: {
enabled: true,
fill: node.color,
stroke: node.color,
size: 5,
},
interpolation: {
type: 'smooth' as const,
},
tooltip: {
renderer: ({ datum, yKey, yName }: { datum: Record<string, unknown>; yKey: string; yName?: string }) => ({
title: yName ?? '',
data: [
{ label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey) },
],
}),
},
}));
return { return {
theme: { theme: {
@ -2004,7 +2015,7 @@ function App() {
bottom: 18, bottom: 18,
left: 24, left: 24,
}, },
data: trendData, data: visibleData,
zoom: { zoom: {
enabled: true, enabled: true,
anchorPointX: 'pointer', anchorPointX: 'pointer',
@ -2087,7 +2098,32 @@ function App() {
] as unknown as NonNullable<NonNullable<AgCartesianChartOptions['annotations']>['toolbar']>['buttons']), ] as unknown as NonNullable<NonNullable<AgCartesianChartOptions['annotations']>['toolbar']>['buttons']),
}, },
}, },
series, series: selectedContentNodes.map((node) => ({
type: 'line',
xKey: 'groupName',
yKey: getSeriesValueKey(node.contentKey, node.id),
yName: `${node.label} ${seriesValueLabel}`,
stroke: node.color,
strokeWidth: 2,
connectMissingData: true,
marker: {
enabled: true,
fill: node.color,
stroke: node.color,
size: 5,
},
interpolation: {
type: 'smooth',
},
tooltip: {
renderer: ({ datum, yKey, yName }) => ({
title: yName,
data: [
{ label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey) },
],
}),
},
})),
axes: { axes: {
x: { x: {
type: 'category', type: 'category',
@ -2162,6 +2198,7 @@ function App() {
selectedValueKey, selectedValueKey,
statisticKey, statisticKey,
]); ]);
const renderMetricSwitcher = (variant: 'chart' | 'grid') => ( const renderMetricSwitcher = (variant: 'chart' | 'grid') => (
<div className={`metric-switcher metric-switcher--${variant}`}> <div className={`metric-switcher metric-switcher--${variant}`}>
<button <button
@ -2239,16 +2276,13 @@ function App() {
type="button" type="button"
title="清空全部筛选" title="清空全部筛选"
onClick={() => { onClick={() => {
const shouldReloadIndicatorTree = !isDefaultTemplateSelection(appliedFilters.templateLibrary);
if (shouldReloadIndicatorTree) {
resetIndicatorTreeState();
}
setAppliedFilters({ setAppliedFilters({
templateLibrary: getDefaultTemplateFilterNodes(), templateLibrary: getDefaultTemplateFilterNodes(),
indicatorTree: [], indicatorTree: defaultIndicatorTreeNodes,
region: [], region: [],
geoLocation: [], geoLocation: [],
facilityType: [], facilityType: [],
buildingFunction: [],
constructionStage: [], constructionStage: [],
planningForm: [], planningForm: [],
}); });

View File

@ -2,8 +2,6 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { AllCommunityModule, ModuleRegistry } from 'ag-charts-community'; import { AllCommunityModule, ModuleRegistry } from 'ag-charts-community';
import App from './App'; import App from './App';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';
import './styles.css'; import './styles.css';
ModuleRegistry.registerModules([AllCommunityModule]); ModuleRegistry.registerModules([AllCommunityModule]);
@ -14,7 +12,7 @@ declare global {
} }
} }
const chartContainerId = 'ztChart'; const chartContainerId = 'sbChart';
const getRootRegistry = () => { const getRootRegistry = () => {
window.__chartReactRoots ??= new WeakMap<Element, ReturnType<typeof createRoot>>(); window.__chartReactRoots ??= new WeakMap<Element, ReturnType<typeof createRoot>>();

View File

@ -64,7 +64,7 @@ button {
position: relative; position: relative;
z-index: 1; z-index: 1;
display: grid; display: grid;
grid-template-columns: minmax(0, 7fr) minmax(320px, 3fr); grid-template-columns: minmax(540px, 52vw) 1fr;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
gap: 12px 28px; gap: 12px 28px;
height: 100vh; height: 100vh;
@ -250,7 +250,7 @@ button {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: grid; display: grid;
grid-template-columns: minmax(0, 7fr) minmax(320px, 3fr); grid-template-columns: minmax(540px, 52vw) 1fr;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
gap: 12px 28px; gap: 12px 28px;
width: 100vw; width: 100vw;

View File

@ -1 +1 @@
$ vite --host 0.0.0.0 --port "5173" $ vite --host 0.0.0.0 --port "5174"

View File

@ -1,8 +1,8 @@
VITE v7.3.2 ready in 303 ms VITE v7.3.2 ready in 306 ms
➜ Local: http://localhost:5173/ ➜ Local: http://localhost:5174/
➜ Network: http://100.106.162.120:5173/ ➜ Network: http://100.106.162.120:5174/
➜ Network: http://192.168.1.155:5173/ ➜ Network: http://192.168.1.155:5174/
➜ Network: http://172.31.112.1:5173/ ➜ Network: http://172.31.112.1:5174/
 ➜ press h + enter to show help  ➜ press h + enter to show help

View File

@ -1,5 +1,6 @@
VITE v7.3.2 ready in 283 ms VITE v7.3.2 ready in 307 ms
➜ Local: http://127.0.0.1:5173/ ➜ Local: http://127.0.0.1:5173/
 ➜ press h + enter to show help  ➜ press h + enter to show help
16:10:18 [vite] (client) hmr update /src/App.tsx