优化
This commit is contained in:
parent
a891b5bccb
commit
cfc6865986
3
bun.lock
3
bun.lock
@ -8,6 +8,7 @@
|
|||||||
"ag-charts-community": "^13.2.1",
|
"ag-charts-community": "^13.2.1",
|
||||||
"ag-charts-enterprise": "13.2.1",
|
"ag-charts-enterprise": "13.2.1",
|
||||||
"ag-charts-react": "^13.2.1",
|
"ag-charts-react": "^13.2.1",
|
||||||
|
"lucide-react": "^1.14.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
},
|
},
|
||||||
@ -233,6 +234,8 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"ag-charts-community": "^13.2.1",
|
"ag-charts-community": "^13.2.1",
|
||||||
"ag-charts-enterprise": "13.2.1",
|
"ag-charts-enterprise": "13.2.1",
|
||||||
"ag-charts-react": "^13.2.1",
|
"ag-charts-react": "^13.2.1",
|
||||||
|
"lucide-react": "^1.14.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
646
src/App.tsx
646
src/App.tsx
@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { AgCharts } from 'ag-charts-react';
|
import { AgCharts } from 'ag-charts-react';
|
||||||
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, LocateFixed, MapPinned } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
AnnotationsModule,
|
AnnotationsModule,
|
||||||
ContextMenuModule,
|
ContextMenuModule,
|
||||||
@ -15,6 +16,7 @@ LicenseManager.setLicenseKey('[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762
|
|||||||
ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]);
|
ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]);
|
||||||
|
|
||||||
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 statisticOptions = [
|
const statisticOptions = [
|
||||||
{ key: 'minValue', label: '最低值', shortLabel: '低' },
|
{ key: 'minValue', label: '最低值', shortLabel: '低' },
|
||||||
@ -38,6 +40,14 @@ const contentOptions = [
|
|||||||
{ key: 'planningForm', label: '规划形式' },
|
{ key: 'planningForm', label: '规划形式' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const filterOptions = [
|
||||||
|
{ key: 'region', label: '省市区', icon: MapPinned },
|
||||||
|
{ key: 'geoLocation', label: '自然地理区位', icon: LocateFixed },
|
||||||
|
{ key: 'facilityType', label: '设施类别', icon: Building2 },
|
||||||
|
{ key: 'constructionStage', label: '建设阶段', icon: Construction },
|
||||||
|
{ key: 'planningForm', label: '规划形式', icon: LayoutGrid },
|
||||||
|
] as const;
|
||||||
|
|
||||||
const browserTreeDefaults = {
|
const browserTreeDefaults = {
|
||||||
treetype: '256',
|
treetype: '256',
|
||||||
checkStrictly: 'true',
|
checkStrictly: 'true',
|
||||||
@ -118,6 +128,7 @@ const chartLineColors = ['#0078a8', '#d14d72', '#1f8f4d', '#d96f23', '#6b5cc8',
|
|||||||
type StatisticKey = (typeof statisticOptions)[number]['key'];
|
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 GroupKey = 'year';
|
type GroupKey = 'year';
|
||||||
type ApiBuildingFunctionStat = {
|
type ApiBuildingFunctionStat = {
|
||||||
group_key?: string | number | null;
|
group_key?: string | number | null;
|
||||||
@ -156,6 +167,11 @@ type SelectedContentNode = {
|
|||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
type SelectedFilterNode = {
|
||||||
|
id: string;
|
||||||
|
filterKey: FilterKey;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
function formatNumber(value: number, maximumFractionDigits: number) {
|
function formatNumber(value: number, maximumFractionDigits: number) {
|
||||||
return value.toLocaleString('zh-CN', {
|
return value.toLocaleString('zh-CN', {
|
||||||
@ -275,6 +291,81 @@ function normalizeTreeRows(rows: unknown[]): TreeNode[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regionFieldKeys = {
|
||||||
|
provinceId: ['provinceId', 'province_id', 'provinceid', 'sfid', 'sf_id', 'shengId', 'sheng_id', 'sheng'],
|
||||||
|
provinceName: ['provinceName', 'province_name', 'province', 'sfmc', 'sf', 'shengName', 'sheng_name', 'shengmc'],
|
||||||
|
cityId: ['cityId', 'city_id', 'cityid', 'sid', 's_id', 'shiId', 'shi_id', 'shi'],
|
||||||
|
cityName: ['cityName', 'city_name', 'city', 'smc', 's', 'shiName', 'shi_name', 'shimc'],
|
||||||
|
districtId: ['districtId', 'district_id', 'districtid', 'countyId', 'county_id', 'qid', 'q_id', 'xid', 'x_id', 'id'],
|
||||||
|
districtName: ['districtName', 'district_name', 'district', 'countyName', 'county_name', 'county', 'qmc', 'q', 'xmc', 'x', 'name', 'shortname'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function createFilterTreeNode(id: string, label: string, children: TreeNode[] = [], expanded = false): TreeNode {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
hasChildren: children.length > 0,
|
||||||
|
canClick: true,
|
||||||
|
expanded,
|
||||||
|
loading: false,
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFlatRegionRows(rows: unknown[]): TreeNode[] {
|
||||||
|
const sourceRows = rows.filter((row): row is Record<string, unknown> => !!row && typeof row === 'object');
|
||||||
|
const hasRegionShape = sourceRows.some((row) =>
|
||||||
|
readText(row, regionFieldKeys.provinceName) &&
|
||||||
|
readText(row, regionFieldKeys.cityName) &&
|
||||||
|
readText(row, regionFieldKeys.districtName),
|
||||||
|
);
|
||||||
|
if (!hasRegionShape) return [];
|
||||||
|
|
||||||
|
const provinceMap = new Map<string, {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
cityMap: Map<string, {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
districts: Map<string, TreeNode>;
|
||||||
|
}>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
sourceRows.forEach((row) => {
|
||||||
|
const provinceLabel = readText(row, regionFieldKeys.provinceName);
|
||||||
|
const cityLabel = readText(row, regionFieldKeys.cityName);
|
||||||
|
const districtLabel = readText(row, regionFieldKeys.districtName);
|
||||||
|
if (!provinceLabel || !cityLabel || !districtLabel) return;
|
||||||
|
|
||||||
|
const provinceId = `region:province:${readText(row, regionFieldKeys.provinceId) || provinceLabel}`;
|
||||||
|
const cityId = `region:city:${provinceLabel}:${readText(row, regionFieldKeys.cityId) || cityLabel}`;
|
||||||
|
const districtId = `region:district:${readText(row, regionFieldKeys.districtId) || `${provinceLabel}:${cityLabel}:${districtLabel}`}`;
|
||||||
|
|
||||||
|
const province = provinceMap.get(provinceId) ?? {
|
||||||
|
id: provinceId,
|
||||||
|
label: provinceLabel,
|
||||||
|
cityMap: new Map<string, { id: string; label: string; districts: Map<string, TreeNode> }>(),
|
||||||
|
};
|
||||||
|
const city = province.cityMap.get(cityId) ?? {
|
||||||
|
id: cityId,
|
||||||
|
label: cityLabel,
|
||||||
|
districts: new Map<string, TreeNode>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
city.districts.set(districtId, createFilterTreeNode(districtId, districtLabel));
|
||||||
|
province.cityMap.set(cityId, city);
|
||||||
|
provinceMap.set(provinceId, province);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(provinceMap.values()).map((province) => {
|
||||||
|
const cityNodes = Array.from(province.cityMap.values()).map((city) =>
|
||||||
|
createFilterTreeNode(city.id, city.label, Array.from(city.districts.values()), false),
|
||||||
|
);
|
||||||
|
return createFilterTreeNode(province.id, province.label, cityNodes, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateNode(nodes: TreeNode[], nodeId: string, updater: (node: TreeNode) => TreeNode): TreeNode[] {
|
function updateNode(nodes: TreeNode[], nodeId: string, updater: (node: TreeNode) => TreeNode): TreeNode[] {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (node.id === nodeId) return updater(node);
|
if (node.id === nodeId) return updater(node);
|
||||||
@ -289,10 +380,40 @@ function getSelectionKey(contentKey: ContentKey, nodeId: string) {
|
|||||||
return `${contentKey}:${nodeId}`;
|
return `${contentKey}:${nodeId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFilterSelectionKey(filterKey: FilterKey, nodeId: string) {
|
||||||
|
return `${filterKey}:${nodeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getSeriesValueKey(index: number) {
|
function getSeriesValueKey(index: number) {
|
||||||
return `amount${index}`;
|
return `amount${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isContentFilterKey(filterKey: FilterKey): filterKey is ContentKey {
|
||||||
|
return filterKey !== 'region';
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTreeNodesByKeyword(nodes: TreeNode[], keyword: string): TreeNode[] {
|
||||||
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||||
|
if (!normalizedKeyword) return nodes;
|
||||||
|
|
||||||
|
const expandDescendants = (targetNodes: TreeNode[]): TreeNode[] => targetNodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: expandDescendants(node.children),
|
||||||
|
expanded: node.hasChildren || node.children.length > 0 ? true : node.expanded,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
const matched = node.label.toLowerCase().includes(normalizedKeyword);
|
||||||
|
const children = filterTreeNodesByKeyword(node.children, keyword);
|
||||||
|
if (!matched && children.length === 0) return [];
|
||||||
|
return [{
|
||||||
|
...node,
|
||||||
|
children: matched ? expandDescendants(node.children) : children,
|
||||||
|
expanded: true,
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderTreeNodes(
|
function renderTreeNodes(
|
||||||
nodes: TreeNode[],
|
nodes: TreeNode[],
|
||||||
contentKey: ContentKey,
|
contentKey: ContentKey,
|
||||||
@ -340,6 +461,51 @@ function renderTreeNodes(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFilterTreeNodes(
|
||||||
|
nodes: TreeNode[],
|
||||||
|
filterKey: FilterKey,
|
||||||
|
selectedNodeKeys: Set<string>,
|
||||||
|
onToggle: (nodeId: string) => void,
|
||||||
|
onSelect: (node: TreeNode) => void,
|
||||||
|
depth = 0,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ul className="content-tree-list filter-tree-list" role={depth === 0 ? 'tree' : 'group'}>
|
||||||
|
{nodes.map((node) => {
|
||||||
|
const selected = selectedNodeKeys.has(getFilterSelectionKey(filterKey, node.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="content-tree-node" role="treeitem" aria-expanded={node.hasChildren ? node.expanded : undefined} key={node.id}>
|
||||||
|
<div className="content-tree-row filter-tree-row" style={{ paddingLeft: 8 + depth * 18 }}>
|
||||||
|
{node.hasChildren ? (
|
||||||
|
<button className="content-tree-caret" type="button" aria-label={node.expanded ? '收起' : '展开'} onClick={() => onToggle(node.id)}>
|
||||||
|
{node.expanded ? '▾' : '▸'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="content-tree-caret is-leaf" />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="content-tree-select filter-tree-select"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selected}
|
||||||
|
onClick={() => onSelect(node)}
|
||||||
|
title={node.label}
|
||||||
|
>
|
||||||
|
<span className="filter-tree-check" aria-hidden="true" />
|
||||||
|
<span className="content-tree-label">{node.label}</span>
|
||||||
|
</button>
|
||||||
|
{node.loading ? <span className="content-tree-loading">加载中</span> : null}
|
||||||
|
</div>
|
||||||
|
{node.expanded && node.children.length > 0
|
||||||
|
? renderFilterTreeNodes(node.children, filterKey, selectedNodeKeys, onToggle, onSelect, depth + 1)
|
||||||
|
: null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const workspaceRef = useRef<HTMLElement>(null);
|
const workspaceRef = useRef<HTMLElement>(null);
|
||||||
const chartFrameRef = useRef<HTMLDivElement>(null);
|
const chartFrameRef = useRef<HTMLDivElement>(null);
|
||||||
@ -349,6 +515,13 @@ function App() {
|
|||||||
constructionStage: false,
|
constructionStage: false,
|
||||||
planningForm: false,
|
planningForm: false,
|
||||||
});
|
});
|
||||||
|
const filterTreeInitialLoadStartedRef = useRef<Record<FilterKey, boolean>>({
|
||||||
|
region: false,
|
||||||
|
geoLocation: false,
|
||||||
|
facilityType: false,
|
||||||
|
constructionStage: 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 [groupKey, setGroupKey] = useState<GroupKey>('year');
|
||||||
@ -379,11 +552,89 @@ function App() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const [loadingHint, setLoadingHint] = useState('');
|
const [loadingHint, setLoadingHint] = useState('');
|
||||||
|
const [filterTreeByKey, setFilterTreeByKey] = useState<Record<FilterKey, TreeNode[]>>({
|
||||||
|
region: [],
|
||||||
|
geoLocation: [],
|
||||||
|
facilityType: [],
|
||||||
|
constructionStage: [],
|
||||||
|
planningForm: [],
|
||||||
|
});
|
||||||
|
const [filterTreeLoadingByKey, setFilterTreeLoadingByKey] = useState<Record<FilterKey, boolean>>({
|
||||||
|
region: false,
|
||||||
|
geoLocation: false,
|
||||||
|
facilityType: false,
|
||||||
|
constructionStage: false,
|
||||||
|
planningForm: false,
|
||||||
|
});
|
||||||
|
const [filterTreeErrorByKey, setFilterTreeErrorByKey] = useState<Record<FilterKey, string | null>>({
|
||||||
|
region: null,
|
||||||
|
geoLocation: null,
|
||||||
|
facilityType: null,
|
||||||
|
constructionStage: null,
|
||||||
|
planningForm: null,
|
||||||
|
});
|
||||||
|
const [filterSearchTreeByKey, setFilterSearchTreeByKey] = useState<Record<FilterKey, TreeNode[]>>({
|
||||||
|
region: [],
|
||||||
|
geoLocation: [],
|
||||||
|
facilityType: [],
|
||||||
|
constructionStage: [],
|
||||||
|
planningForm: [],
|
||||||
|
});
|
||||||
|
const [filterSearchLoadingByKey, setFilterSearchLoadingByKey] = useState<Record<FilterKey, boolean>>({
|
||||||
|
region: false,
|
||||||
|
geoLocation: false,
|
||||||
|
facilityType: false,
|
||||||
|
constructionStage: false,
|
||||||
|
planningForm: false,
|
||||||
|
});
|
||||||
|
const [filterSearchErrorByKey, setFilterSearchErrorByKey] = useState<Record<FilterKey, string | null>>({
|
||||||
|
region: null,
|
||||||
|
geoLocation: null,
|
||||||
|
facilityType: null,
|
||||||
|
constructionStage: null,
|
||||||
|
planningForm: null,
|
||||||
|
});
|
||||||
|
const [appliedFilters, setAppliedFilters] = useState<Record<FilterKey, SelectedFilterNode[]>>({
|
||||||
|
region: [],
|
||||||
|
geoLocation: [],
|
||||||
|
facilityType: [],
|
||||||
|
constructionStage: [],
|
||||||
|
planningForm: [],
|
||||||
|
});
|
||||||
|
const [filterModalKey, setFilterModalKey] = useState<FilterKey | null>(null);
|
||||||
|
const [draftFilterNodes, setDraftFilterNodes] = useState<SelectedFilterNode[]>([]);
|
||||||
|
const [filterSearchValue, setFilterSearchValue] = useState('');
|
||||||
|
const filterSearchComposingRef = useRef(false);
|
||||||
|
const filterSearchTimerRef = useRef<number | null>(null);
|
||||||
|
const filterSearchRequestSeqRef = useRef<Record<FilterKey, number>>({
|
||||||
|
region: 0,
|
||||||
|
geoLocation: 0,
|
||||||
|
facilityType: 0,
|
||||||
|
constructionStage: 0,
|
||||||
|
planningForm: 0,
|
||||||
|
});
|
||||||
|
const lastFilterSearchRef = useRef('');
|
||||||
|
|
||||||
const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
|
const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
|
||||||
const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
|
const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
|
||||||
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 activeFilterTree = filterModalKey ? filterTreeByKey[filterModalKey] : [];
|
||||||
|
const trimmedFilterSearchValue = filterSearchValue.trim();
|
||||||
|
const activeFilterDisplayTree = filterModalKey && trimmedFilterSearchValue ? filterSearchTreeByKey[filterModalKey] : activeFilterTree;
|
||||||
|
const activeFilterTreeLoading = filterModalKey
|
||||||
|
? trimmedFilterSearchValue ? filterSearchLoadingByKey[filterModalKey] : filterTreeLoadingByKey[filterModalKey]
|
||||||
|
: false;
|
||||||
|
const activeFilterTreeError = filterModalKey
|
||||||
|
? trimmedFilterSearchValue ? filterSearchErrorByKey[filterModalKey] : filterTreeErrorByKey[filterModalKey]
|
||||||
|
: null;
|
||||||
|
const activeFilterCount = Object.values(appliedFilters).reduce((total, nodes) => total + nodes.length, 0);
|
||||||
|
const chartEmptyText = selectedContentNodes.length === 0
|
||||||
|
? '请选择右侧分类项'
|
||||||
|
: activeFilterCount > 0
|
||||||
|
? '当前筛选无数据'
|
||||||
|
: '所选分类暂无数据';
|
||||||
const selectedValueKey = metricKey === 'dataCount' ? 'dataCount' : statisticKey;
|
const selectedValueKey = metricKey === 'dataCount' ? 'dataCount' : statisticKey;
|
||||||
const requestMetricKey = metricKey === 'dataCount' ? 'cost' : metricKey;
|
const requestMetricKey = metricKey === 'dataCount' ? 'cost' : metricKey;
|
||||||
const seriesValueLabel = metricKey === 'dataCount' ? selectedMetric.label : selectedStatistic.label;
|
const seriesValueLabel = metricKey === 'dataCount' ? selectedMetric.label : selectedStatistic.label;
|
||||||
@ -391,6 +642,19 @@ function App() {
|
|||||||
() => new Set(selectedContentNodes.map((node) => getSelectionKey(node.contentKey, node.id))),
|
() => new Set(selectedContentNodes.map((node) => getSelectionKey(node.contentKey, node.id))),
|
||||||
[selectedContentNodes],
|
[selectedContentNodes],
|
||||||
);
|
);
|
||||||
|
const draftFilterNodeKeys = useMemo(
|
||||||
|
() => new Set(draftFilterNodes.map((node) => getFilterSelectionKey(node.filterKey, node.id))),
|
||||||
|
[draftFilterNodes],
|
||||||
|
);
|
||||||
|
const appliedFilterPayload = useMemo(
|
||||||
|
() => filterOptions
|
||||||
|
.map((option) => ({
|
||||||
|
key: option.key,
|
||||||
|
nodes: appliedFilters[option.key].map((node) => ({ nodeId: node.id })),
|
||||||
|
}))
|
||||||
|
.filter((filter) => filter.nodes.length > 0),
|
||||||
|
[appliedFilters],
|
||||||
|
);
|
||||||
|
|
||||||
const getNodeColor = (contentKey: ContentKey, nodeId: string) => {
|
const getNodeColor = (contentKey: ContentKey, nodeId: string) => {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
@ -401,7 +665,7 @@ function App() {
|
|||||||
return chartLineColors[hash];
|
return chartLineColors[hash];
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchContentTree = async (contentKey: ContentKey, nodeId?: string) => {
|
const fetchContentTree = async (contentKey: ContentKey, nodeId?: string, signal?: AbortSignal) => {
|
||||||
const config = contentTreeConfigs[contentKey];
|
const config = contentTreeConfigs[contentKey];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('接口待接入');
|
throw new Error('接口待接入');
|
||||||
@ -428,6 +692,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
const response = await fetch(`${config.endpoint}?${buildQuery(params)}`, {
|
const response = await fetch(`${config.endpoint}?${buildQuery(params)}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
signal,
|
||||||
headers: {
|
headers: {
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
},
|
},
|
||||||
@ -435,7 +700,65 @@ function App() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
return normalizeTreeRows(pickArray(await response.json()));
|
const rows = pickArray(await response.json());
|
||||||
|
return normalizeTreeRows(rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeBackendTree = (nodes: TreeNode[]): TreeNode[] => nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
hasChildren: node.children.length > 0 || node.hasChildren,
|
||||||
|
expanded: node.children.length > 0,
|
||||||
|
loaded: true,
|
||||||
|
children: normalizeBackendTree(node.children),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getTreeNodePrefix = (nodes: TreeNode[]) => {
|
||||||
|
const stack = [...nodes];
|
||||||
|
while (stack.length) {
|
||||||
|
const node = stack.shift();
|
||||||
|
if (!node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const matched = node.id.match(/^(.*_)\d+$/);
|
||||||
|
if (matched) {
|
||||||
|
return matched[1];
|
||||||
|
}
|
||||||
|
stack.push(...node.children);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRegionFilterTree = async (signal?: AbortSignal) => {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTree?${buildQuery({ key: 'region' })}`, {
|
||||||
|
signal,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const rows = pickArray(await response.json());
|
||||||
|
const flatRegionTree = normalizeFlatRegionRows(rows);
|
||||||
|
return flatRegionTree.length > 0 ? flatRegionTree : normalizeBackendTree(normalizeTreeRows(rows));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBackendFilterTreeSearch = async (filterKey: FilterKey, keyword: string, signal?: AbortSignal) => {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTreeSearch?${buildQuery({
|
||||||
|
key: filterKey,
|
||||||
|
keyword,
|
||||||
|
nodePrefix: isContentFilterKey(filterKey) ? getTreeNodePrefix(filterTreeByKey[filterKey]) : '',
|
||||||
|
})}`, {
|
||||||
|
signal,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => '');
|
||||||
|
throw new Error(`搜索接口请求失败:HTTP ${response.status}${errorText ? ` ${errorText.slice(0, 160)}` : ''}`);
|
||||||
|
}
|
||||||
|
return normalizeBackendTree(normalizeTreeRows(pickArray(await response.json())));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadContentTreeWithDefaultExpansion = async (contentKey: ContentKey) => {
|
const loadContentTreeWithDefaultExpansion = async (contentKey: ContentKey) => {
|
||||||
@ -463,6 +786,17 @@ function App() {
|
|||||||
return loadChildren(await fetchContentTree(contentKey), 1);
|
return loadChildren(await fetchContentTree(contentKey), 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadFilterTree = async (filterKey: FilterKey, keyword?: string, signal?: AbortSignal) => {
|
||||||
|
if (filterKey === 'region') {
|
||||||
|
const nodes = await fetchRegionFilterTree(signal);
|
||||||
|
return keyword?.trim() ? filterTreeNodesByKeyword(nodes, keyword) : nodes;
|
||||||
|
}
|
||||||
|
if (keyword?.trim()) {
|
||||||
|
return fetchBackendFilterTreeSearch(filterKey, keyword, signal);
|
||||||
|
}
|
||||||
|
return loadContentTreeWithDefaultExpansion(filterKey);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleContentNode = (nodeId: string) => {
|
const toggleContentNode = (nodeId: string) => {
|
||||||
if (!contentTreeConfigs[activeContentKey]) return;
|
if (!contentTreeConfigs[activeContentKey]) return;
|
||||||
const target = activeTree.find((node) => node.id === nodeId);
|
const target = activeTree.find((node) => node.id === nodeId);
|
||||||
@ -546,6 +880,196 @@ function App() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureFilterTreeLoaded = (filterKey: FilterKey) => {
|
||||||
|
if (filterTreeByKey[filterKey].length > 0 || filterTreeInitialLoadStartedRef.current[filterKey]) return;
|
||||||
|
|
||||||
|
filterTreeInitialLoadStartedRef.current[filterKey] = true;
|
||||||
|
setFilterTreeLoadingByKey((current) => ({ ...current, [filterKey]: true }));
|
||||||
|
setFilterTreeErrorByKey((current) => ({ ...current, [filterKey]: null }));
|
||||||
|
loadFilterTree(filterKey)
|
||||||
|
.then((nodes) => {
|
||||||
|
setFilterTreeByKey((current) => ({ ...current, [filterKey]: nodes }));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
filterTreeInitialLoadStartedRef.current[filterKey] = false;
|
||||||
|
setFilterTreeErrorByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[filterKey]: error instanceof Error ? error.message : '加载失败',
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setFilterTreeLoadingByKey((current) => ({ ...current, [filterKey]: false }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFilterModal = (filterKey: FilterKey) => {
|
||||||
|
setFilterModalKey(filterKey);
|
||||||
|
setDraftFilterNodes(appliedFilters[filterKey]);
|
||||||
|
setFilterSearchValue('');
|
||||||
|
lastFilterSearchRef.current = '';
|
||||||
|
if (filterSearchTimerRef.current != null) {
|
||||||
|
window.clearTimeout(filterSearchTimerRef.current);
|
||||||
|
filterSearchTimerRef.current = null;
|
||||||
|
}
|
||||||
|
ensureFilterTreeLoaded(filterKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFilterModal = () => {
|
||||||
|
if (filterModalKey) {
|
||||||
|
filterSearchRequestSeqRef.current[filterModalKey] += 1;
|
||||||
|
}
|
||||||
|
if (filterSearchTimerRef.current != null) {
|
||||||
|
window.clearTimeout(filterSearchTimerRef.current);
|
||||||
|
filterSearchTimerRef.current = null;
|
||||||
|
}
|
||||||
|
lastFilterSearchRef.current = '';
|
||||||
|
setFilterModalKey(null);
|
||||||
|
setDraftFilterNodes([]);
|
||||||
|
setFilterSearchValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleFilterSearch = (filterKey: FilterKey, rawKeyword: string) => {
|
||||||
|
if (filterSearchTimerRef.current != null) {
|
||||||
|
window.clearTimeout(filterSearchTimerRef.current);
|
||||||
|
filterSearchTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = rawKeyword.trim();
|
||||||
|
const searchKey = `${filterKey}:${keyword}`;
|
||||||
|
if (keyword && lastFilterSearchRef.current === searchKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastFilterSearchRef.current = searchKey;
|
||||||
|
filterSearchRequestSeqRef.current[filterKey] += 1;
|
||||||
|
const requestSeq = filterSearchRequestSeqRef.current[filterKey];
|
||||||
|
|
||||||
|
if (!keyword) {
|
||||||
|
setFilterSearchTreeByKey((current) => ({ ...current, [filterKey]: [] }));
|
||||||
|
setFilterSearchErrorByKey((current) => ({ ...current, [filterKey]: null }));
|
||||||
|
setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: false }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSearchTimerRef.current = window.setTimeout(() => {
|
||||||
|
setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: true }));
|
||||||
|
setFilterSearchErrorByKey((current) => ({ ...current, [filterKey]: null }));
|
||||||
|
void loadFilterTree(filterKey, keyword)
|
||||||
|
.then((nodes) => {
|
||||||
|
if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
|
||||||
|
setFilterSearchTreeByKey((current) => ({ ...current, [filterKey]: nodes }));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
|
||||||
|
setFilterSearchErrorByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[filterKey]: error instanceof Error ? error.message : '加载失败',
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
|
||||||
|
setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: false }));
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFilterTreeNode = (nodeId: string) => {
|
||||||
|
if (!filterModalKey) return;
|
||||||
|
const visit = (nodes: TreeNode[]): TreeNode | null => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === nodeId) return node;
|
||||||
|
const matched = visit(node.children);
|
||||||
|
if (matched) return matched;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const node = visit(filterTreeByKey[filterModalKey]);
|
||||||
|
if (!node?.hasChildren) return;
|
||||||
|
|
||||||
|
setFilterTreeByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[filterModalKey]: updateNode(current[filterModalKey], nodeId, (currentNode) => ({
|
||||||
|
...currentNode,
|
||||||
|
expanded: currentNode.loaded ? !currentNode.expanded : currentNode.expanded,
|
||||||
|
loading: currentNode.loaded ? currentNode.loading : true,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (node.loaded || !isContentFilterKey(filterModalKey)) return;
|
||||||
|
|
||||||
|
const currentFilterKey = filterModalKey;
|
||||||
|
fetchContentTree(currentFilterKey, nodeId)
|
||||||
|
.then((children) => {
|
||||||
|
setFilterTreeByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[currentFilterKey]: updateNode(current[currentFilterKey], nodeId, (currentNode) => ({
|
||||||
|
...currentNode,
|
||||||
|
children,
|
||||||
|
expanded: true,
|
||||||
|
loading: false,
|
||||||
|
loaded: true,
|
||||||
|
hasChildren: currentNode.hasChildren || children.length > 0,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setFilterTreeByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[currentFilterKey]: updateNode(current[currentFilterKey], nodeId, (currentNode) => ({
|
||||||
|
...currentNode,
|
||||||
|
loading: false,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
setFilterTreeErrorByKey((current) => ({
|
||||||
|
...current,
|
||||||
|
[currentFilterKey]: error instanceof Error ? error.message : '加载失败',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDraftFilterNode = (node: TreeNode) => {
|
||||||
|
if (!filterModalKey) return;
|
||||||
|
const currentFilterKey = filterModalKey;
|
||||||
|
setDraftFilterNodes((current) => {
|
||||||
|
const selectionKey = getFilterSelectionKey(currentFilterKey, node.id);
|
||||||
|
const exists = current.some((item) => getFilterSelectionKey(item.filterKey, item.id) === selectionKey);
|
||||||
|
if (exists) {
|
||||||
|
return current.filter((item) => getFilterSelectionKey(item.filterKey, item.id) !== selectionKey);
|
||||||
|
}
|
||||||
|
return [...current, { id: node.id, filterKey: currentFilterKey, label: node.label }];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilterModal = () => {
|
||||||
|
if (!filterModalKey) return;
|
||||||
|
setAppliedFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
[filterModalKey]: draftFilterNodes,
|
||||||
|
}));
|
||||||
|
setChartDataBySelection({});
|
||||||
|
setLoadError(null);
|
||||||
|
if (selectedContentNodes.length > 0) {
|
||||||
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
setChartQueryVersion((version) => version + 1);
|
||||||
|
closeFilterModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilter = (filterKey: FilterKey) => {
|
||||||
|
if (appliedFilters[filterKey].length === 0) return;
|
||||||
|
setAppliedFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
[filterKey]: [],
|
||||||
|
}));
|
||||||
|
setChartDataBySelection({});
|
||||||
|
setLoadError(null);
|
||||||
|
if (selectedContentNodes.length > 0) {
|
||||||
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
setChartQueryVersion((version) => version + 1);
|
||||||
|
};
|
||||||
|
|
||||||
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
const updateMetricKey = (nextMetricKey: MetricKey) => {
|
||||||
setMetricKey(nextMetricKey);
|
setMetricKey(nextMetricKey);
|
||||||
setChartDataBySelection({});
|
setChartDataBySelection({});
|
||||||
@ -612,6 +1136,7 @@ function App() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
groupBy: groupKey,
|
groupBy: groupKey,
|
||||||
metric: requestMetricKey,
|
metric: requestMetricKey,
|
||||||
|
filters: appliedFilterPayload,
|
||||||
nodes: selectedContentNodes.map((node) => ({
|
nodes: selectedContentNodes.map((node) => ({
|
||||||
key: getSelectionKey(node.contentKey, node.id),
|
key: getSelectionKey(node.contentKey, node.id),
|
||||||
contentKey: node.contentKey,
|
contentKey: node.contentKey,
|
||||||
@ -646,7 +1171,7 @@ function App() {
|
|||||||
return () => {
|
return () => {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
};
|
};
|
||||||
}, [chartQueryVersion, groupKey, metricKey, requestMetricKey, selectedContentNodes]);
|
}, [appliedFilterPayload, chartQueryVersion, groupKey, metricKey, requestMetricKey, selectedContentNodes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const frame = chartFrameRef.current;
|
const frame = chartFrameRef.current;
|
||||||
@ -831,7 +1356,7 @@ function App() {
|
|||||||
},
|
},
|
||||||
overlays: {
|
overlays: {
|
||||||
noData: {
|
noData: {
|
||||||
text: '请选择右侧分类项',
|
text: chartEmptyText,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
annotations: {
|
annotations: {
|
||||||
@ -955,7 +1480,7 @@ function App() {
|
|||||||
pagination: true,
|
pagination: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [chartDataBySelection, metricKey, requestMetricKey, selectedContentNodes, selectedMetric.label, seriesValueLabel, selectedValueKey, statisticKey]);
|
}, [activeFilterCount, chartDataBySelection, chartEmptyText, metricKey, requestMetricKey, selectedContentNodes, selectedMetric.label, seriesValueLabel, selectedValueKey, statisticKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="dashboard-shell">
|
<main className="dashboard-shell">
|
||||||
@ -966,6 +1491,51 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="workspace" aria-label="年度费用模板" ref={workspaceRef}>
|
<section className="workspace" aria-label="年度费用模板" ref={workspaceRef}>
|
||||||
|
<div className="chart-filter-bar chart-filter-bar--workspace" aria-label="筛选条件">
|
||||||
|
{filterOptions.map((option) => {
|
||||||
|
const count = appliedFilters[option.key].length;
|
||||||
|
const FilterIcon = option.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="chart-filter-button"
|
||||||
|
type="button"
|
||||||
|
key={option.key}
|
||||||
|
aria-pressed={count > 0}
|
||||||
|
title={count > 0 ? `${option.label}:已选${count}项` : option.label}
|
||||||
|
onClick={() => openFilterModal(option.key)}
|
||||||
|
>
|
||||||
|
<FilterIcon className="chart-filter-icon" aria-hidden="true" strokeWidth={2} />
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{count > 0 ? <strong>{count}</strong> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{activeFilterCount > 0 ? (
|
||||||
|
<button
|
||||||
|
className="chart-filter-clear"
|
||||||
|
type="button"
|
||||||
|
title="清空全部筛选"
|
||||||
|
onClick={() => {
|
||||||
|
setAppliedFilters({
|
||||||
|
region: [],
|
||||||
|
geoLocation: [],
|
||||||
|
facilityType: [],
|
||||||
|
constructionStage: [],
|
||||||
|
planningForm: [],
|
||||||
|
});
|
||||||
|
setChartDataBySelection({});
|
||||||
|
setLoadError(null);
|
||||||
|
if (selectedContentNodes.length > 0) {
|
||||||
|
setLoadingHint('正在按筛选条件重新计算');
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
setChartQueryVersion((version) => version + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<section className="chart-area" aria-label="年度总费用图表">
|
<section className="chart-area" aria-label="年度总费用图表">
|
||||||
<div className="chart-frame" ref={chartFrameRef}>
|
<div className="chart-frame" ref={chartFrameRef}>
|
||||||
{statisticMenuOpen ? (
|
{statisticMenuOpen ? (
|
||||||
@ -1064,6 +1634,72 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</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([])}>
|
||||||
|
清空当前
|
||||||
|
</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}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
267
src/styles.css
267
src/styles.css
@ -65,9 +65,10 @@ button {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(540px, 52vw) 1fr;
|
grid-template-columns: minmax(540px, 52vw) 1fr;
|
||||||
gap: 28px;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 12px 28px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 8px 28px 18px 64px;
|
padding: 30px 28px 18px 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-area {
|
.chart-area {
|
||||||
@ -157,7 +158,84 @@ button {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 26px 0 0;
|
padding: 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-bar--workspace {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 0 28px 0 64px;
|
||||||
|
padding-right: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-button,
|
||||||
|
.chart-filter-clear {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 32px;
|
||||||
|
max-width: 136px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.18);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #46413b;
|
||||||
|
background: rgba(255, 249, 241, 0.76);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 30px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-icon {
|
||||||
|
flex: 0 0 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-button span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-button strong {
|
||||||
|
display: inline-grid;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: #fff;
|
||||||
|
background: #0078a8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-button:hover,
|
||||||
|
.chart-filter-button[aria-pressed="true"],
|
||||||
|
.chart-filter-clear:hover {
|
||||||
|
color: #0078a8;
|
||||||
|
border-color: rgba(0, 120, 168, 0.36);
|
||||||
|
background: rgba(255, 252, 248, 0.96);
|
||||||
|
box-shadow: 0 1px 5px rgba(69, 54, 36, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-filter-clear {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #7a3f2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace:fullscreen {
|
.workspace:fullscreen {
|
||||||
@ -165,7 +243,8 @@ button {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(540px, 52vw) 1fr;
|
grid-template-columns: minmax(540px, 52vw) 1fr;
|
||||||
gap: 28px;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 12px 28px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 8px 28px 18px 64px;
|
padding: 8px 28px 18px 64px;
|
||||||
@ -173,7 +252,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace:fullscreen .chart-area {
|
.workspace:fullscreen .chart-area {
|
||||||
height: calc(100vh - 26px);
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace:fullscreen .right-panel {
|
.workspace:fullscreen .right-panel {
|
||||||
@ -182,6 +261,11 @@ button {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace:fullscreen .chart-filter-bar--workspace {
|
||||||
|
margin: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-frame > div {
|
.chart-frame > div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -193,7 +277,7 @@ button {
|
|||||||
|
|
||||||
.chart-loading-mask {
|
.chart-loading-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 26px 0 0;
|
inset: 16px 0 0;
|
||||||
z-index: 13;
|
z-index: 13;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -688,6 +772,177 @@ button {
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 28px;
|
||||||
|
background: rgba(35, 30, 25, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
|
||||||
|
width: min(560px, calc(100vw - 56px));
|
||||||
|
height: min(680px, calc(100vh - 56px));
|
||||||
|
min-height: 420px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff7ef;
|
||||||
|
box-shadow: 0 18px 50px rgba(35, 30, 25, 0.22);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 16px 18px 12px;
|
||||||
|
border-bottom: 1px solid rgba(90, 82, 72, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #262a33;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-close {
|
||||||
|
display: grid;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6d6258;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-close:hover {
|
||||||
|
color: #0078a8;
|
||||||
|
border-color: rgba(0, 120, 168, 0.28);
|
||||||
|
background: rgba(255, 252, 248, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-search {
|
||||||
|
padding: 12px 18px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-search input {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #262a33;
|
||||||
|
background: #fffdfa;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-search input:focus {
|
||||||
|
border-color: rgba(0, 120, 168, 0.46);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 120, 168, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-selected {
|
||||||
|
padding: 0 18px 8px;
|
||||||
|
color: #6d6258;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-tree {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 4px 12px 12px;
|
||||||
|
border-top: 1px solid rgba(90, 82, 72, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tree-row {
|
||||||
|
min-height: 36px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tree-select {
|
||||||
|
height: 32px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tree-check {
|
||||||
|
position: relative;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.32);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 252, 248, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tree-select[aria-pressed="true"] .filter-tree-check {
|
||||||
|
border-color: #0078a8;
|
||||||
|
background: #0078a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tree-select[aria-pressed="true"] .filter-tree-check::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
top: 1px;
|
||||||
|
width: 5px;
|
||||||
|
height: 9px;
|
||||||
|
border-right: 2px solid #fff;
|
||||||
|
border-bottom: 2px solid #fff;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 18px 16px;
|
||||||
|
border-top: 1px solid rgba(90, 82, 72, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-actions button {
|
||||||
|
height: 32px;
|
||||||
|
min-width: 72px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.18);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 249, 241, 0.76);
|
||||||
|
color: #46413b;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-actions button:hover {
|
||||||
|
color: #0078a8;
|
||||||
|
border-color: rgba(0, 120, 168, 0.36);
|
||||||
|
background: rgba(255, 252, 248, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-confirm {
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: #0078a8 !important;
|
||||||
|
background: #0078a8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-modal-confirm:hover {
|
||||||
|
background: #006f9b !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
body {
|
body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
76
vite-dev.log
76
vite-dev.log
@ -113,3 +113,79 @@ Port 5173 is in use, trying another one...
|
|||||||
[2m10:04:35[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
[2m10:04:35[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
[2m10:06:06[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
[2m10:06:06[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
[2m10:06:25[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
[2m10:06:25[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m14:21:06[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:26:18[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:26:51[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m14:28:15[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:28:24[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:07:03[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:07:31[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:08:11[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:13:59[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:47:31[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:48:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:48:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:00:07[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:15:44[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:06:45[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:07:01[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:07:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:07:43[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:08:03[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:08:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:08:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:09:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:09:41[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m18:10:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m18:11:00[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:08:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:09:58[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:10:10[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:12:48[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:24:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:24:54[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:25:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:25:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:31:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:32:05[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:32:12[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:32:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:45:56[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:00:51[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:01:08[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:01:23[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:01:36[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:01:46[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:01:56[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:02:58[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:08:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:08:56[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:09:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m10:09:29[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m10:12:15[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m10:17:02[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:54:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:54:44[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m10:59:54[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:00:16[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:00:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:00:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:12:15[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:16:52[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:20:34[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:22:39[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:30:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:42:14[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m11:48:40[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m12:07:11[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m12:07:25[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m12:07:50[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:44:40[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:47:46[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:48:15[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m14:51:05[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:13:54[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:14:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:17:54[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:18:05[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m15:18:58[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user