1
This commit is contained in:
parent
36fee1f291
commit
5b540acade
BIN
.gitnexus/lbug
BIN
.gitnexus/lbug
Binary file not shown.
Binary file not shown.
23
.playwright-mcp/console-2026-05-07T01-42-43-218Z.log
Normal file
23
.playwright-mcp/console-2026-05-07T01-42-43-218Z.log
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[ 239614ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 240878ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 241566ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 242175ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 242757ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 244362ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 254208ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 632838ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 637178ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 687035ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 690304ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 739631ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 772552ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 772670ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 773677ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 773698ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 795038ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 796833ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 796944ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 807892ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 807915ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 809704ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
|
[ 809719ms] [WARNING] AG Charts - Option `annotations.toolbar.buttons[0].value` cannot be set to `"statistic-select"`; expecting a keyword such as 'line-menu', 'fibonacci-menu', 'text-menu', 'shape-menu', 'measurer-menu', 'line', 'horizontal-line', 'vertical-line', 'parallel-channel', 'disjoint-channel', 'fibonacci-retracement', 'fibonacci-retracement-trend-based', 'text', 'comment', 'callout', 'note' or 'clear', ignoring. @ http://localhost:5174/node_modules/.vite/deps/chunk-XDC3NMYR.js?v=2852bbef:834
|
||||||
203
src/App.tsx
203
src/App.tsx
@ -14,56 +14,138 @@ import { AG_CHARTS_LOCALE_ZH_CN } from 'ag-charts-locale';
|
|||||||
LicenseManager.setLicenseKey('[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b');
|
LicenseManager.setLicenseKey('[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b');
|
||||||
ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]);
|
ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]);
|
||||||
|
|
||||||
const yearlyCostData = [
|
const API_BASE_URL = 'http://127.0.0.1:9089/api/v1';
|
||||||
{ year: '2021', cost: 2200000, buildingArea: 18600, builtArea: 17200, usableArea: 14800 },
|
|
||||||
{ year: '2022', cost: 2500000, buildingArea: 19100, builtArea: 17600, usableArea: 15300 },
|
|
||||||
{ year: '2023', cost: 3450000, buildingArea: 20600, builtArea: 19000, usableArea: 16400 },
|
|
||||||
{ year: '2024', cost: 9600000, buildingArea: 24800, builtArea: 23200, usableArea: 20100 },
|
|
||||||
{ year: '2025', cost: null, buildingArea: null, builtArea: null, usableArea: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
const metricOptions = [
|
const statisticOptions = [
|
||||||
{ key: 'cost', label: '造价(元)', shortLabel: '造价' },
|
{ key: 'minValue', label: '最低值', shortLabel: '低' },
|
||||||
{ key: 'buildingArea', label: '建筑面积指标(元/m²)', shortLabel: '建筑' },
|
{ key: 'maxValue', label: '最高值', shortLabel: '高' },
|
||||||
{ key: 'builtArea', label: '建造面积指标(元/m²)', shortLabel: '建造' },
|
{ key: 'avgValue', label: '平均值', shortLabel: '均' },
|
||||||
{ key: 'usableArea', label: '使用面积指标(元/m²)', shortLabel: '使用' },
|
{ key: 'medianValue', label: '中位数', shortLabel: '中' },
|
||||||
|
{ key: 'dataCount', label: '数据量', shortLabel: '量' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const metricOptions = [
|
||||||
|
{ key: 'cost', label: '造价(元)' },
|
||||||
|
{ key: 'buildingArea', label: '建筑面积指标(元/m²)' },
|
||||||
|
{ key: 'builtArea', label: '建造面积指标(元/m²)' },
|
||||||
|
{ key: 'usableArea', label: '使用面积指标(元/m²)' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type StatisticKey = (typeof statisticOptions)[number]['key'];
|
||||||
type MetricKey = (typeof metricOptions)[number]['key'];
|
type MetricKey = (typeof metricOptions)[number]['key'];
|
||||||
|
type GroupKey = 'year';
|
||||||
|
type ApiBuildingFunctionStat = {
|
||||||
|
group_key?: string | number | null;
|
||||||
|
group_name?: string | null;
|
||||||
|
min_value?: number | null;
|
||||||
|
max_value?: number | null;
|
||||||
|
avg_value?: number | null;
|
||||||
|
median_value?: number | null;
|
||||||
|
data_count?: number | null;
|
||||||
|
};
|
||||||
|
type ChartDatum = {
|
||||||
|
groupName: string;
|
||||||
|
minValue: number | null;
|
||||||
|
maxValue: number | null;
|
||||||
|
avgValue: number | null;
|
||||||
|
medianValue: number | null;
|
||||||
|
dataCount: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
function formatWan(value: number) {
|
function formatWan(value: number) {
|
||||||
return `${Math.round(value / 10000).toLocaleString('zh-CN')}万`;
|
return `${Math.round(value / 10000).toLocaleString('zh-CN')}万`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum {
|
||||||
|
return {
|
||||||
|
groupName: row.group_name || String(row.group_key ?? '未命名'),
|
||||||
|
minValue: row.min_value ?? null,
|
||||||
|
maxValue: row.max_value ?? null,
|
||||||
|
avgValue: row.avg_value ?? null,
|
||||||
|
medianValue: row.median_value ?? null,
|
||||||
|
dataCount: row.data_count ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const workspaceRef = useRef<HTMLElement>(null);
|
const workspaceRef = useRef<HTMLElement>(null);
|
||||||
const chartFrameRef = useRef<HTMLDivElement>(null);
|
const chartFrameRef = useRef<HTMLDivElement>(null);
|
||||||
|
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 [statisticMenuOpen, setStatisticMenuOpen] = useState(false);
|
||||||
const [metricMenuOpen, setMetricMenuOpen] = useState(false);
|
const [metricMenuOpen, setMetricMenuOpen] = useState(false);
|
||||||
|
const [chartData, setChartData] = useState<ChartDatum[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
setLoading(true);
|
||||||
|
setLoadError(null);
|
||||||
|
try {
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
groupBy: groupKey,
|
||||||
|
metric: metricKey,
|
||||||
|
});
|
||||||
|
const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStats?${search.toString()}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const payload = (await response.json()) as { data?: ApiBuildingFunctionStat[] };
|
||||||
|
setChartData((payload.data ?? []).map(normalizeStat).slice(0, 36));
|
||||||
|
} catch (error) {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
setLoadError(error instanceof Error ? error.message : '接口请求失败');
|
||||||
|
setChartData([]);
|
||||||
|
} finally {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadStats();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [groupKey, metricKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const frame = chartFrameRef.current;
|
const frame = chartFrameRef.current;
|
||||||
const fullscreenTarget = workspaceRef.current;
|
const fullscreenTarget = workspaceRef.current;
|
||||||
if (!frame || !fullscreenTarget) return;
|
if (!frame || !fullscreenTarget) return;
|
||||||
|
|
||||||
const getFullscreenButton = () => frame.querySelector<HTMLButtonElement>('.chart-fullscreen-button');
|
const getFullscreenButton = () => frame.querySelector<HTMLButtonElement>('.chart-fullscreen-button');
|
||||||
|
const getStatisticButton = () => frame.querySelector<HTMLElement>('.ag-charts-myButton-statistic')?.closest<HTMLButtonElement>('.ag-charts-toolbar__button');
|
||||||
|
|
||||||
const syncFullscreenButton = () => {
|
const syncToolbarButtons = () => {
|
||||||
const button = getFullscreenButton();
|
const button = getFullscreenButton();
|
||||||
if (!button) return;
|
if (button) {
|
||||||
|
let icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
|
||||||
|
if (!icon) {
|
||||||
|
button.innerHTML = '<i class="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button"></i>';
|
||||||
|
icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
|
||||||
|
}
|
||||||
|
|
||||||
let icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
|
const isFullscreen = document.fullscreenElement === fullscreenTarget;
|
||||||
if (!icon) {
|
button.classList.toggle('ag-charts-toolbar__button--active', isFullscreen);
|
||||||
button.innerHTML = '<i class="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button"></i>';
|
icon?.classList.toggle('anticon-arrow-salt', !isFullscreen);
|
||||||
icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
|
icon?.classList.toggle('anticon-shrink', isFullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFullscreen = document.fullscreenElement === fullscreenTarget;
|
const statisticButton = getStatisticButton();
|
||||||
button.classList.toggle('ag-charts-toolbar__button--active', isFullscreen);
|
if (statisticButton) {
|
||||||
icon?.classList.toggle('anticon-arrow-salt', !isFullscreen);
|
statisticButton.classList.add('chart-statistic-button');
|
||||||
icon?.classList.toggle('anticon-shrink', isFullscreen);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
@ -83,19 +165,24 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
syncFullscreenButton();
|
syncToolbarButtons();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolbarClick = (event: MouseEvent) => {
|
const handleToolbarClick = (event: MouseEvent) => {
|
||||||
const target = event.target as Element | null;
|
const target = event.target as Element | null;
|
||||||
const button = target?.closest<HTMLButtonElement>(
|
const button = target?.closest<HTMLButtonElement>(
|
||||||
'.chart-fullscreen-button',
|
'.chart-fullscreen-button, .chart-statistic-button',
|
||||||
);
|
);
|
||||||
if (!button || !frame.contains(button)) return;
|
if (!button || !frame.contains(button)) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleFullscreen();
|
if (button.classList.contains('chart-statistic-button')) {
|
||||||
|
setMetricMenuOpen(false);
|
||||||
|
setStatisticMenuOpen((open) => !open);
|
||||||
|
} else {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolbarKeyDown = (event: KeyboardEvent) => {
|
const handleToolbarKeyDown = (event: KeyboardEvent) => {
|
||||||
@ -103,20 +190,25 @@ function App() {
|
|||||||
|
|
||||||
const target = event.target as Element | null;
|
const target = event.target as Element | null;
|
||||||
const button = target?.closest<HTMLButtonElement>(
|
const button = target?.closest<HTMLButtonElement>(
|
||||||
'.chart-fullscreen-button',
|
'.chart-fullscreen-button, .chart-statistic-button',
|
||||||
);
|
);
|
||||||
if (!button || !frame.contains(button)) return;
|
if (!button || !frame.contains(button)) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleFullscreen();
|
if (button.classList.contains('chart-statistic-button')) {
|
||||||
|
setMetricMenuOpen(false);
|
||||||
|
setStatisticMenuOpen((open) => !open);
|
||||||
|
} else {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const suppressBrowserContextMenu = (event: MouseEvent) => {
|
const suppressBrowserContextMenu = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver(syncFullscreenButton);
|
const observer = new MutationObserver(syncToolbarButtons);
|
||||||
document.addEventListener('keydown', handleKeyDown, true);
|
document.addEventListener('keydown', handleKeyDown, true);
|
||||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
document.addEventListener('contextmenu', suppressBrowserContextMenu);
|
document.addEventListener('contextmenu', suppressBrowserContextMenu);
|
||||||
@ -124,7 +216,7 @@ function App() {
|
|||||||
frame.addEventListener('click', handleToolbarClick, true);
|
frame.addEventListener('click', handleToolbarClick, true);
|
||||||
frame.addEventListener('keydown', handleToolbarKeyDown, true);
|
frame.addEventListener('keydown', handleToolbarKeyDown, true);
|
||||||
observer.observe(frame, { childList: true, subtree: true });
|
observer.observe(frame, { childList: true, subtree: true });
|
||||||
syncFullscreenButton();
|
syncToolbarButtons();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown, true);
|
document.removeEventListener('keydown', handleKeyDown, true);
|
||||||
@ -138,10 +230,10 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const chartOptions = useMemo<AgCartesianChartOptions>(() => {
|
const chartOptions = useMemo<AgCartesianChartOptions>(() => {
|
||||||
const isCost = metricKey === 'cost';
|
const isCount = statisticKey === 'dataCount';
|
||||||
const chartData = yearlyCostData.map((datum) => ({
|
const visibleData = chartData.map((datum) => ({
|
||||||
year: datum.year,
|
groupName: datum.groupName,
|
||||||
amount: datum.cost == null ? null : isCost ? datum.cost : Math.round((datum.cost / Number(datum[metricKey])) * 100) / 100,
|
amount: datum[statisticKey],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -172,7 +264,7 @@ function App() {
|
|||||||
bottom: 18,
|
bottom: 18,
|
||||||
left: 24,
|
left: 24,
|
||||||
},
|
},
|
||||||
data: chartData,
|
data: visibleData,
|
||||||
zoom: {
|
zoom: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
anchorPointX: 'pointer',
|
anchorPointX: 'pointer',
|
||||||
@ -196,7 +288,12 @@ function App() {
|
|||||||
annotations: {
|
annotations: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
buttons: [
|
buttons: ([
|
||||||
|
{
|
||||||
|
value: 'statistic-select',
|
||||||
|
tooltip: '切换统计指标',
|
||||||
|
label: `<span class="ag-charts-myButton-statistic ag-charts-diy-button">${selectedStatistic.shortLabel}</span>`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'trend-line-drawing',
|
icon: 'trend-line-drawing',
|
||||||
value: 'line-menu',
|
value: 'line-menu',
|
||||||
@ -222,15 +319,15 @@ function App() {
|
|||||||
value: 'clear',
|
value: 'clear',
|
||||||
tooltip: 'Clear annotations',
|
tooltip: 'Clear annotations',
|
||||||
},
|
},
|
||||||
],
|
] as unknown as NonNullable<NonNullable<AgCartesianChartOptions['annotations']>['toolbar']>['buttons']),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xKey: 'year',
|
xKey: 'groupName',
|
||||||
yKey: 'amount',
|
yKey: 'amount',
|
||||||
yName: selectedMetric.label,
|
yName: `${selectedMetric.label} ${selectedStatistic.label}`,
|
||||||
stroke: '#0078a8',
|
stroke: '#0078a8',
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
marker: {
|
marker: {
|
||||||
@ -273,7 +370,7 @@ function App() {
|
|||||||
color: '#1f2933',
|
color: '#1f2933',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
formatter: ({ value }) =>
|
formatter: ({ value }) =>
|
||||||
isCost
|
!isCount
|
||||||
? formatWan(Number(value))
|
? formatWan(Number(value))
|
||||||
: Number(value).toLocaleString('zh-CN', {
|
: Number(value).toLocaleString('zh-CN', {
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
@ -307,7 +404,7 @@ function App() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [metricKey, selectedMetric.label]);
|
}, [chartData, selectedMetric.label, selectedStatistic.label, statisticKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="dashboard-shell">
|
<main className="dashboard-shell">
|
||||||
@ -320,6 +417,25 @@ function App() {
|
|||||||
<section className="workspace" aria-label="年度费用模板" ref={workspaceRef}>
|
<section className="workspace" aria-label="年度费用模板" ref={workspaceRef}>
|
||||||
<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 ? (
|
||||||
|
<div className="statistic-switcher-menu" role="menu" aria-label="切换统计指标">
|
||||||
|
{statisticOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
className="statistic-switcher-menu-item"
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
key={option.key}
|
||||||
|
aria-current={option.key === statisticKey}
|
||||||
|
onClick={() => {
|
||||||
|
setStatisticKey(option.key);
|
||||||
|
setStatisticMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="metric-switcher">
|
<div className="metric-switcher">
|
||||||
<button
|
<button
|
||||||
className="metric-switcher-button"
|
className="metric-switcher-button"
|
||||||
@ -327,7 +443,11 @@ function App() {
|
|||||||
title="切换纵坐标指标"
|
title="切换纵坐标指标"
|
||||||
aria-expanded={metricMenuOpen}
|
aria-expanded={metricMenuOpen}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
onClick={() => setMetricMenuOpen((open) => !open)}
|
aria-label={`纵坐标:${selectedMetric.label}`}
|
||||||
|
onClick={() => {
|
||||||
|
setStatisticMenuOpen(false);
|
||||||
|
setMetricMenuOpen((open) => !open);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{selectedMetric.label}
|
{selectedMetric.label}
|
||||||
</button>
|
</button>
|
||||||
@ -351,6 +471,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{loading || loadError ? <div className="chart-status">{loading ? '加载中' : loadError}</div> : null}
|
||||||
<button className="chart-fullscreen-button ag-charts-toolbar__button" type="button" title="全屏(F11)">
|
<button className="chart-fullscreen-button ag-charts-toolbar__button" type="button" title="全屏(F11)">
|
||||||
<i className="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button" />
|
<i className="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -186,6 +186,11 @@ button {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-frame > .statistic-switcher-menu,
|
||||||
|
.chart-frame > .chart-status {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-frame .ag-charts-wrapper {
|
.chart-frame .ag-charts-wrapper {
|
||||||
--ag-charts-accent-color: #0078a8;
|
--ag-charts-accent-color: #0078a8;
|
||||||
--ag-charts-button-background-color: rgba(255, 249, 241, 0.72);
|
--ag-charts-button-background-color: rgba(255, 249, 241, 0.72);
|
||||||
@ -212,6 +217,41 @@ button {
|
|||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statistic-switcher-menu {
|
||||||
|
position: absolute;
|
||||||
|
left: 70px;
|
||||||
|
top: 24px;
|
||||||
|
z-index: 14;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 86px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.22);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fbede1;
|
||||||
|
box-shadow: 0 4px 14px rgba(69, 54, 36, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-switcher-menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
color: #262a33;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-switcher-menu-item:hover,
|
||||||
|
.statistic-switcher-menu-item[aria-current="true"] {
|
||||||
|
color: #0078a8;
|
||||||
|
background: rgba(255, 252, 248, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
.metric-switcher {
|
.metric-switcher {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 74px;
|
left: 74px;
|
||||||
@ -284,10 +324,24 @@ button {
|
|||||||
background: rgba(255, 252, 248, 0.94);
|
background: rgba(255, 252, 248, 0.94);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 62px;
|
||||||
|
left: 74px;
|
||||||
|
z-index: 11;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid rgba(90, 82, 72, 0.16);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #262a33;
|
||||||
|
background: rgba(255, 252, 248, 0.94);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-frame .chart-fullscreen-button {
|
.chart-frame .chart-fullscreen-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 24px;
|
left: 24px;
|
||||||
top: 210px;
|
top: 252px;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
|
|||||||
28
vite-dev.log
28
vite-dev.log
@ -36,3 +36,31 @@ Port 5173 is in use, trying another one...
|
|||||||
[2m16:33:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
[2m16:33:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
[2m16:38:49[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
[2m16:38:49[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
[2m16:39:34[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
[2m16:39:34[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m16:46:57[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:47:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:53:41[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m16:54:18[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m16:59:35[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:01:23[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:03:24[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:06:50[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:29:44[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:29:52[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:32:17[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:32:43[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:34:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:40:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:40:37[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m17:41:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:42:39[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m17:44:42[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:11:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:11:59[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:14:42[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:15:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:18:16[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:18:28[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:35:28[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:35:53[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/App.tsx[22m
|
||||||
|
[2m09:35:53[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/styles.css[22m
|
||||||
|
[2m09:36:43[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