@hsu-react/ui 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/components/Chart/Bar/index.d.ts +5 -1
- package/es/components/Chart/Bar/index.js +84 -9
- package/es/components/Chart/Line/index.d.ts +5 -1
- package/es/components/Chart/Line/index.js +84 -9
- package/es/components/Chart/_utils/autoSmooth.d.ts +2 -0
- package/es/components/Chart/_utils/autoSmooth.js +12 -2
- package/es/components/Chart/_utils/cartesian.d.ts +11 -0
- package/es/components/Chart/_utils/cartesian.js +19 -0
- package/es/components/Chart/chartUtils/chartTooltipFormatter.d.ts +2 -0
- package/es/components/Chart/chartUtils/chartTooltipFormatter.js +13 -3
- package/es/components/Chart/chartUtils/index.d.ts +5 -2
- package/es/components/Chart/chartUtils/index.js +3 -1
- package/es/components/Chart/chartUtils/useDataZoomWindow.d.ts +19 -0
- package/es/components/Chart/chartUtils/useDataZoomWindow.js +37 -0
- package/es/components/Chart/chartUtils/useLegendSelection.d.ts +14 -0
- package/es/components/Chart/chartUtils/useLegendSelection.js +35 -0
- package/es/components/Modal/index.module.scss +1 -0
- package/es/components/Panel/ListPanel/ListModalPanel/index.js +33 -5
- package/es/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
- package/es/components/Panel/ListPanel/index.module.scss +11 -12
- package/lib/components/Chart/Bar/index.d.ts +5 -1
- package/lib/components/Chart/Bar/index.js +68 -4
- package/lib/components/Chart/Line/index.d.ts +5 -1
- package/lib/components/Chart/Line/index.js +68 -4
- package/lib/components/Chart/_utils/autoSmooth.d.ts +2 -0
- package/lib/components/Chart/_utils/autoSmooth.js +11 -1
- package/lib/components/Chart/_utils/cartesian.d.ts +11 -0
- package/lib/components/Chart/_utils/cartesian.js +21 -1
- package/lib/components/Chart/chartUtils/chartTooltipFormatter.d.ts +2 -0
- package/lib/components/Chart/chartUtils/chartTooltipFormatter.js +5 -3
- package/lib/components/Chart/chartUtils/index.d.ts +5 -2
- package/lib/components/Chart/chartUtils/index.js +14 -0
- package/lib/components/Chart/chartUtils/useDataZoomWindow.d.ts +19 -0
- package/lib/components/Chart/chartUtils/useDataZoomWindow.js +31 -0
- package/lib/components/Chart/chartUtils/useLegendSelection.d.ts +14 -0
- package/lib/components/Chart/chartUtils/useLegendSelection.js +26 -0
- package/lib/components/Modal/index.module.scss +1 -0
- package/lib/components/Panel/ListPanel/ListModalPanel/index.js +21 -2
- package/lib/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
- package/lib/components/Panel/ListPanel/index.module.scss +11 -12
- package/package.json +2 -2
- package/src/components/Chart/Bar/index.tsx +132 -2
- package/src/components/Chart/Line/index.tsx +132 -2
- package/src/components/Chart/_utils/autoSmooth.ts +181 -164
- package/src/components/Chart/_utils/cartesian.ts +28 -0
- package/src/components/Chart/chartUtils/chartTooltipFormatter.ts +11 -2
- package/src/components/Chart/chartUtils/index.ts +53 -37
- package/src/components/Chart/chartUtils/useDataZoomWindow.ts +61 -0
- package/src/components/Chart/chartUtils/useLegendSelection.ts +35 -0
- package/src/components/Modal/index.module.scss +1 -0
- package/src/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
- package/src/components/Panel/ListPanel/ListModalPanel/index.tsx +261 -233
- package/src/components/Panel/ListPanel/index.module.scss +11 -12
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DataZoomIndexWindow } from "../_utils/cartesian";
|
|
2
|
+
export interface DataZoomWindowOptions {
|
|
3
|
+
/** 开关:是否按 dataZoom 当前可见窗口重算坐标轴,默认开启 */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface DataZoomWindowResult {
|
|
7
|
+
/** 当前可见窗口;开关关闭或尚未收到回调时为 null(表示全量) */
|
|
8
|
+
zoomWindow: DataZoomIndexWindow | null;
|
|
9
|
+
/** 传给 Chart.Bar / Chart.Line 的 onDataZoomWindowChanged;开关关闭时为 undefined(不绑定事件) */
|
|
10
|
+
onDataZoomWindowChanged?: (window: DataZoomIndexWindow) => void;
|
|
11
|
+
/** 按当前可见窗口裁剪系列数据,配合 calculateAxisConfig 只对展示部分重算坐标轴 */
|
|
12
|
+
sliceByZoomWindow: <T>(values?: T[] | null) => T[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* dataZoom 可见窗口管理:配合 Chart.Bar / Chart.Line 的 onDataZoomWindowChanged,
|
|
16
|
+
* 在调用方按"当前可见窗口"裁剪数据后重算 Y 轴 min/max/interval(calculateAxisConfig),
|
|
17
|
+
* 实现滑块拖动/滚轮/自动滚动时坐标轴跟随展示部分动态重算;可与 useLegendSelection 叠加。
|
|
18
|
+
*/
|
|
19
|
+
export declare function useDataZoomWindow(options?: DataZoomWindowOptions): DataZoomWindowResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useDataZoomWindow = useDataZoomWindow;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* dataZoom 可见窗口管理:配合 Chart.Bar / Chart.Line 的 onDataZoomWindowChanged,
|
|
10
|
+
* 在调用方按"当前可见窗口"裁剪数据后重算 Y 轴 min/max/interval(calculateAxisConfig),
|
|
11
|
+
* 实现滑块拖动/滚轮/自动滚动时坐标轴跟随展示部分动态重算;可与 useLegendSelection 叠加。
|
|
12
|
+
*/
|
|
13
|
+
function useDataZoomWindow(options) {
|
|
14
|
+
const enabled = options?.enabled ?? true;
|
|
15
|
+
const [zoomWindow, setZoomWindow] = (0, _react.useState)(null);
|
|
16
|
+
const onDataZoomWindowChanged = (0, _react.useCallback)(next => {
|
|
17
|
+
// 窗口未变化时保持原引用,避免重渲染 → 同步回调 → 再重渲染的循环
|
|
18
|
+
setZoomWindow(prev => prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex ? prev : next);
|
|
19
|
+
}, []);
|
|
20
|
+
const effectiveWindow = enabled ? zoomWindow : null;
|
|
21
|
+
const sliceByZoomWindow = (0, _react.useCallback)(values => {
|
|
22
|
+
if (!values) return [];
|
|
23
|
+
if (!effectiveWindow) return values;
|
|
24
|
+
return values.slice(effectiveWindow.startIndex, effectiveWindow.endIndex + 1);
|
|
25
|
+
}, [effectiveWindow]);
|
|
26
|
+
return {
|
|
27
|
+
zoomWindow: effectiveWindow,
|
|
28
|
+
onDataZoomWindowChanged: enabled ? onDataZoomWindowChanged : undefined,
|
|
29
|
+
sliceByZoomWindow
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LegendSelectionResult {
|
|
2
|
+
/** 传给图表 legend.selected,保证重渲染(setOption notMerge)后隐藏状态不丢 */
|
|
3
|
+
legendSelected: Record<string, boolean>;
|
|
4
|
+
/** 传给图表 onLegendSelectChanged */
|
|
5
|
+
onLegendSelectChanged: (selected: Record<string, boolean>) => void;
|
|
6
|
+
/** 判断某系列当前是否可见(未点过的系列默认可见) */
|
|
7
|
+
isSeriesVisible: (name?: string | null) => boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 图例选中状态管理:配合 Chart.Bar / Chart.Line 的 onLegendSelectChanged,
|
|
11
|
+
* 在调用方按"当前可见系列"重算 Y 轴 min/max/interval(calculateAxisConfig),
|
|
12
|
+
* 实现图例隐藏某一项后坐标轴自动重算。
|
|
13
|
+
*/
|
|
14
|
+
export declare function useLegendSelection(): LegendSelectionResult;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useLegendSelection = useLegendSelection;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* 图例选中状态管理:配合 Chart.Bar / Chart.Line 的 onLegendSelectChanged,
|
|
10
|
+
* 在调用方按"当前可见系列"重算 Y 轴 min/max/interval(calculateAxisConfig),
|
|
11
|
+
* 实现图例隐藏某一项后坐标轴自动重算。
|
|
12
|
+
*/
|
|
13
|
+
function useLegendSelection() {
|
|
14
|
+
const [legendSelected, setLegendSelected] = (0, _react.useState)({});
|
|
15
|
+
const onLegendSelectChanged = (0, _react.useCallback)(selected => {
|
|
16
|
+
setLegendSelected({
|
|
17
|
+
...selected
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
const isSeriesVisible = (0, _react.useCallback)(name => name == null || legendSelected[name] !== false, [legendSelected]);
|
|
21
|
+
return {
|
|
22
|
+
legendSelected,
|
|
23
|
+
onLegendSelectChanged,
|
|
24
|
+
isSeriesVisible
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -7,6 +7,7 @@ exports.default = void 0;
|
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _Search = _interopRequireDefault(require("../../../Search"));
|
|
9
9
|
var _Table = _interopRequireDefault(require("../../../Table"));
|
|
10
|
+
var _icons = require("@ant-design/icons");
|
|
10
11
|
var _classnames = _interopRequireDefault(require("classnames"));
|
|
11
12
|
var _lodash = require("lodash");
|
|
12
13
|
var _indexModule = _interopRequireDefault(require("./index.module.scss"));
|
|
@@ -49,6 +50,12 @@ const ListModalPanel = props => {
|
|
|
49
50
|
} = (0, _usePermissions.default)(hasPermi);
|
|
50
51
|
const [_columns, setColumns] = (0, _react.useState)(columns);
|
|
51
52
|
const [showColumnMgt, setShowColumnMgt] = (0, _react.useState)(false);
|
|
53
|
+
const [isFullscreen, setIsFullscreen] = (0, _react.useState)(false);
|
|
54
|
+
(0, _react.useEffect)(() => {
|
|
55
|
+
if (modalConfig.open) {
|
|
56
|
+
setIsFullscreen(false);
|
|
57
|
+
}
|
|
58
|
+
}, [modalConfig.open]);
|
|
52
59
|
(0, _react.useEffect)(() => {
|
|
53
60
|
if (columnMgt && columns?.length) {
|
|
54
61
|
const _columns = columns?.map(col => {
|
|
@@ -109,9 +116,20 @@ const ListModalPanel = props => {
|
|
|
109
116
|
}
|
|
110
117
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
111
118
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Modal.default, {
|
|
112
|
-
width: 1400,
|
|
113
|
-
className: (0, _classnames.default)(modalClassName),
|
|
114
119
|
...modalConfig,
|
|
120
|
+
width: isFullscreen ? "100vw" : modalConfig.width ?? 1400,
|
|
121
|
+
...(isFullscreen ? {
|
|
122
|
+
full: true
|
|
123
|
+
} : {}),
|
|
124
|
+
className: (0, _classnames.default)(_indexModule.default.modal, modalClassName, {
|
|
125
|
+
[_indexModule.default.fullscreen]: isFullscreen
|
|
126
|
+
}),
|
|
127
|
+
titleButtonGroup: [{
|
|
128
|
+
type: "text",
|
|
129
|
+
icon: isFullscreen ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.FullscreenExitOutlined, {}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.FullscreenOutlined, {}),
|
|
130
|
+
title: isFullscreen ? "退出全屏" : "全屏",
|
|
131
|
+
onClick: () => setIsFullscreen(v => !v)
|
|
132
|
+
}, ...(modalConfig.titleButtonGroup ?? [])],
|
|
115
133
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
116
134
|
className: (0, _classnames.default)(_indexModule.default.ListModalPanel, className),
|
|
117
135
|
children: [extraContent, SearchComponent, /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
@@ -130,6 +148,7 @@ const ListModalPanel = props => {
|
|
|
130
148
|
scroll: true,
|
|
131
149
|
serialNumberColumn: true,
|
|
132
150
|
bordered: true,
|
|
151
|
+
fillPanel: true,
|
|
133
152
|
...tableConfig,
|
|
134
153
|
columns: _columns
|
|
135
154
|
})]
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
.modal {
|
|
2
|
+
// 定高让内部“height: 100%”链路可解析(antd Modal 默认内容自适应,内层设置 height/max-height 无效)
|
|
3
|
+
height: 700px;
|
|
4
|
+
|
|
5
|
+
&.fullscreen {
|
|
6
|
+
top: 0;
|
|
7
|
+
|
|
8
|
+
width: 100vw !important;
|
|
9
|
+
max-width: 100vw !important;
|
|
10
|
+
height: 100vh !important;
|
|
11
|
+
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
.ListModalPanel {
|
|
2
18
|
display: flex;
|
|
3
19
|
flex-direction: column;
|
|
4
20
|
row-gap: 10px;
|
|
5
21
|
|
|
6
|
-
height:
|
|
7
|
-
max-height: 70vh;
|
|
22
|
+
height: 100%;
|
|
8
23
|
|
|
9
24
|
.search {
|
|
10
25
|
padding: 0px;
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
|
|
88
88
|
width: 100%;
|
|
89
89
|
min-height: 0px;
|
|
90
|
-
padding:
|
|
90
|
+
padding: 10px;
|
|
91
91
|
|
|
92
92
|
background: var(--cf-surface);
|
|
93
93
|
|
|
@@ -102,23 +102,22 @@
|
|
|
102
102
|
align-items: center;
|
|
103
103
|
justify-content: space-between;
|
|
104
104
|
column-gap: 10px;
|
|
105
|
+
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
// 页面级页签:无底色容器,页签拆成独立圆角块、块间留空;
|
|
108
|
+
// margin/radius 需 !important 压过 TabBar 内部的连体分段样式(负 margin + 首尾切角)
|
|
109
|
+
.headerTabBar {
|
|
110
|
+
column-gap: 4px;
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
> div {
|
|
113
|
+
margin-left: 0 !important;
|
|
114
|
+
border-radius: 6px !important;
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
background: var(--cf-surface);
|
|
117
|
+
}
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
&.hasHeaderTabBar {
|
|
115
|
-
.search {
|
|
116
|
-
padding: 10px;
|
|
117
|
-
margin-top: -10px;
|
|
118
|
-
|
|
119
|
-
border-radius: 0px 0px 8px 8px;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
121
|
&:not(:has(.search)) {
|
|
123
122
|
.headerTabBarContainer {
|
|
124
123
|
border-radius: 8px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hsu-react/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "一套基于 React + Ant Design 的中后台业务组件库",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@emotion/cache": "^11.14.0",
|
|
64
64
|
"@emotion/react": "^11.14.0",
|
|
65
65
|
"@emotion/styled": "^11.14.1",
|
|
66
|
-
"@hsu-react/single-router": "
|
|
66
|
+
"@hsu-react/single-router": "0.0.26",
|
|
67
67
|
"@iconify/json": "^2.2.385",
|
|
68
68
|
"@iconify/react": "^5.0.1",
|
|
69
69
|
"@uiw/codemirror-theme-vscode": "^4.25.1",
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
ChartScrollConfig,
|
|
17
17
|
createDefaultCategoryXAxis,
|
|
18
18
|
createDefaultValueYAxis,
|
|
19
|
+
DataZoomIndexWindow,
|
|
19
20
|
getSliderShow,
|
|
21
|
+
percentWindowToIndexWindow,
|
|
20
22
|
resolveScrollConfig,
|
|
21
23
|
} from "../_utils/cartesian";
|
|
22
24
|
|
|
@@ -27,6 +29,10 @@ export interface ChartBarProps extends ChartCommonProps {
|
|
|
27
29
|
legendData?: string[];
|
|
28
30
|
scrollConfig?: ChartScrollConfig;
|
|
29
31
|
onClick?: (event: echarts.ECElementEvent) => void;
|
|
32
|
+
/** 图例点选变化回调,参数为当前各系列选中状态(配合 legend.selected 可实现坐标轴重算) */
|
|
33
|
+
onLegendSelectChanged?: (selected: Record<string, boolean>) => void;
|
|
34
|
+
/** dataZoom 可见窗口变化回调(滑块/滚轮/自动滚动/初始同步均触发),参数为当前可见的 x 轴索引区间(配合 useDataZoomWindow 可按展示部分重算坐标轴) */
|
|
35
|
+
onDataZoomWindowChanged?: (window: DataZoomIndexWindow) => void;
|
|
30
36
|
/** 是否启用图例自动滚动,默认 false */
|
|
31
37
|
enableLegendAutoScroll?: boolean;
|
|
32
38
|
/** 图例每页可见数量,默认 8 */
|
|
@@ -55,6 +61,8 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
55
61
|
insideDataZoom,
|
|
56
62
|
sliderDataZoom,
|
|
57
63
|
onClick,
|
|
64
|
+
onLegendSelectChanged,
|
|
65
|
+
onDataZoomWindowChanged,
|
|
58
66
|
enableLegendAutoScroll = false,
|
|
59
67
|
legendVisibleCount = 8,
|
|
60
68
|
legendScrollInterval = 1500,
|
|
@@ -63,6 +71,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
63
71
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
64
72
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
65
73
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
|
74
|
+
const prevXAxisKeyRef = useRef<string | null>(null);
|
|
66
75
|
const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
|
|
67
76
|
null,
|
|
68
77
|
);
|
|
@@ -268,8 +277,46 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
268
277
|
chartInstanceRef.current = chart;
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
//
|
|
280
|
+
// 设置配置:notMerge 会把 dataZoom 窗口重置回初始位置,
|
|
281
|
+
// x 轴数据未变化时(如坐标轴重算/图例点选触发的重渲染)先记录当前窗口,设置后恢复
|
|
282
|
+
const xAxisKey = xAxisData?.join("\u0001") ?? "";
|
|
283
|
+
const prevZoomRanges =
|
|
284
|
+
prevXAxisKeyRef.current === xAxisKey
|
|
285
|
+
? ((chart.getOption() as ChartsOption | undefined)?.dataZoom as
|
|
286
|
+
| Array<{ start?: number; end?: number }>
|
|
287
|
+
| undefined)
|
|
288
|
+
: undefined;
|
|
289
|
+
prevXAxisKeyRef.current = xAxisKey;
|
|
272
290
|
chart.setOption(chartOption as ChartOptionType, true);
|
|
291
|
+
const nextZooms = (chartOption as ChartsOption).dataZoom;
|
|
292
|
+
if (
|
|
293
|
+
prevZoomRanges?.length &&
|
|
294
|
+
Array.isArray(nextZooms) &&
|
|
295
|
+
nextZooms.length === prevZoomRanges.length
|
|
296
|
+
) {
|
|
297
|
+
chart.setOption({
|
|
298
|
+
dataZoom: prevZoomRanges.map((z) => ({ start: z.start, end: z.end })),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 向调用方同步当前可见窗口(初始渲染与恢复后),用于按展示部分重算坐标轴
|
|
303
|
+
if (onDataZoomWindowChanged) {
|
|
304
|
+
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
305
|
+
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
306
|
+
const zoom = zooms?.[0];
|
|
307
|
+
const totalLen = xAxisData?.length ?? 0;
|
|
308
|
+
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
309
|
+
onDataZoomWindowChanged(
|
|
310
|
+
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
// 无 dataZoom(如数据量不足未启用滚动)时窗口即全量,同步一次避免残留旧窗口
|
|
314
|
+
onDataZoomWindowChanged({
|
|
315
|
+
startIndex: 0,
|
|
316
|
+
endIndex: Math.max(0, totalLen - 1),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
273
320
|
|
|
274
321
|
// 添加 resize 监听
|
|
275
322
|
window.addEventListener("resize", handleResize);
|
|
@@ -285,6 +332,59 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
285
332
|
chartInstanceRef.current?.on("click", onClick);
|
|
286
333
|
}
|
|
287
334
|
|
|
335
|
+
// 图例点选事件
|
|
336
|
+
const handleLegendSelectChanged = (params: unknown) => {
|
|
337
|
+
onLegendSelectChanged?.(
|
|
338
|
+
(params as { selected: Record<string, boolean> }).selected,
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
if (onLegendSelectChanged) {
|
|
342
|
+
chartInstanceRef.current?.on(
|
|
343
|
+
"legendselectchanged",
|
|
344
|
+
handleLegendSelectChanged,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// dataZoom 原生交互(slider 拖动 / inside 缩放)→ 同步可见窗口
|
|
349
|
+
const handleDataZoom = (params: unknown) => {
|
|
350
|
+
const raw = params as {
|
|
351
|
+
start?: number;
|
|
352
|
+
end?: number;
|
|
353
|
+
startValue?: number;
|
|
354
|
+
endValue?: number;
|
|
355
|
+
batch?: Array<{
|
|
356
|
+
start?: number;
|
|
357
|
+
end?: number;
|
|
358
|
+
startValue?: number;
|
|
359
|
+
endValue?: number;
|
|
360
|
+
}>;
|
|
361
|
+
};
|
|
362
|
+
const info = raw?.batch?.[0] ?? raw;
|
|
363
|
+
if (
|
|
364
|
+
typeof info?.startValue === "number" &&
|
|
365
|
+
typeof info?.endValue === "number"
|
|
366
|
+
) {
|
|
367
|
+
onDataZoomWindowChanged?.({
|
|
368
|
+
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
369
|
+
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
370
|
+
});
|
|
371
|
+
} else if (
|
|
372
|
+
typeof info?.start === "number" &&
|
|
373
|
+
typeof info?.end === "number"
|
|
374
|
+
) {
|
|
375
|
+
onDataZoomWindowChanged?.(
|
|
376
|
+
percentWindowToIndexWindow(
|
|
377
|
+
info.start,
|
|
378
|
+
info.end,
|
|
379
|
+
xAxisData?.length ?? 0,
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
if (onDataZoomWindowChanged) {
|
|
385
|
+
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
386
|
+
}
|
|
387
|
+
|
|
288
388
|
// 图例自动滚动(参考 Pie 组件)
|
|
289
389
|
const opt = chartOption as ChartsOption;
|
|
290
390
|
const legendDataLen = Array.isArray(opt.legend)
|
|
@@ -317,6 +417,17 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
317
417
|
chartInstanceRef.current?.off("click", onClick);
|
|
318
418
|
}
|
|
319
419
|
|
|
420
|
+
if (onLegendSelectChanged) {
|
|
421
|
+
chartInstanceRef.current?.off(
|
|
422
|
+
"legendselectchanged",
|
|
423
|
+
handleLegendSelectChanged,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (onDataZoomWindowChanged) {
|
|
428
|
+
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
429
|
+
}
|
|
430
|
+
|
|
320
431
|
if (legendScrollRef.current) {
|
|
321
432
|
legendScrollRef.current.dispose();
|
|
322
433
|
legendScrollRef.current = null;
|
|
@@ -326,6 +437,9 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
326
437
|
chartOption,
|
|
327
438
|
handleResize,
|
|
328
439
|
onClick,
|
|
440
|
+
onLegendSelectChanged,
|
|
441
|
+
onDataZoomWindowChanged,
|
|
442
|
+
xAxisData,
|
|
329
443
|
enableLegendAutoScroll,
|
|
330
444
|
legendVisibleCount,
|
|
331
445
|
legendScrollInterval,
|
|
@@ -358,10 +472,26 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
358
472
|
startIndex: normalizedScroll.startIndex,
|
|
359
473
|
autoPlay: normalizedScroll.autoScroll,
|
|
360
474
|
enableWheelScroll,
|
|
475
|
+
onWindowChange: onDataZoomWindowChanged
|
|
476
|
+
? (startPercent, endPercent) =>
|
|
477
|
+
onDataZoomWindowChanged(
|
|
478
|
+
percentWindowToIndexWindow(
|
|
479
|
+
startPercent,
|
|
480
|
+
endPercent,
|
|
481
|
+
xAxisData.length,
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
: undefined,
|
|
361
485
|
});
|
|
362
486
|
|
|
363
487
|
return dispose;
|
|
364
|
-
}, [
|
|
488
|
+
}, [
|
|
489
|
+
dataZoom,
|
|
490
|
+
normalizedScroll,
|
|
491
|
+
sliderDataZoom,
|
|
492
|
+
xAxisData,
|
|
493
|
+
onDataZoomWindowChanged,
|
|
494
|
+
]);
|
|
365
495
|
|
|
366
496
|
// 组件卸载时清理资源
|
|
367
497
|
useEffect(() => {
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
ChartScrollConfig,
|
|
17
17
|
createDefaultCategoryXAxis,
|
|
18
18
|
createDefaultValueYAxis,
|
|
19
|
+
DataZoomIndexWindow,
|
|
19
20
|
getSliderShow,
|
|
21
|
+
percentWindowToIndexWindow,
|
|
20
22
|
resolveScrollConfig,
|
|
21
23
|
} from "../_utils/cartesian";
|
|
22
24
|
|
|
@@ -28,6 +30,10 @@ export interface ChartLineProps extends ChartCommonProps {
|
|
|
28
30
|
scrollConfig?: ChartScrollConfig;
|
|
29
31
|
yAxisNameOffsets?: number[];
|
|
30
32
|
onClick?: (event: echarts.ECElementEvent) => void;
|
|
33
|
+
/** 图例点选变化回调,参数为当前各系列选中状态(配合 legend.selected 可实现坐标轴重算) */
|
|
34
|
+
onLegendSelectChanged?: (selected: Record<string, boolean>) => void;
|
|
35
|
+
/** dataZoom 可见窗口变化回调(滑块/滚轮/自动滚动/初始同步均触发),参数为当前可见的 x 轴索引区间(配合 useDataZoomWindow 可按展示部分重算坐标轴) */
|
|
36
|
+
onDataZoomWindowChanged?: (window: DataZoomIndexWindow) => void;
|
|
31
37
|
/** 是否启用图例自动滚动,默认 false */
|
|
32
38
|
enableLegendAutoScroll?: boolean;
|
|
33
39
|
/** 图例每页可见数量,默认 8 */
|
|
@@ -56,6 +62,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
56
62
|
insideDataZoom,
|
|
57
63
|
sliderDataZoom,
|
|
58
64
|
onClick,
|
|
65
|
+
onLegendSelectChanged,
|
|
66
|
+
onDataZoomWindowChanged,
|
|
59
67
|
enableLegendAutoScroll = false,
|
|
60
68
|
legendVisibleCount = 8,
|
|
61
69
|
legendScrollInterval = 1500,
|
|
@@ -64,6 +72,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
64
72
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
65
73
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
66
74
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
|
75
|
+
const prevXAxisKeyRef = useRef<string | null>(null);
|
|
67
76
|
const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
|
|
68
77
|
null,
|
|
69
78
|
);
|
|
@@ -265,8 +274,46 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
265
274
|
chartInstanceRef.current = chart;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
//
|
|
277
|
+
// 设置配置:notMerge 会把 dataZoom 窗口重置回初始位置,
|
|
278
|
+
// x 轴数据未变化时(如坐标轴重算/图例点选触发的重渲染)先记录当前窗口,设置后恢复
|
|
279
|
+
const xAxisKey = xAxisData?.join("\u0001") ?? "";
|
|
280
|
+
const prevZoomRanges =
|
|
281
|
+
prevXAxisKeyRef.current === xAxisKey
|
|
282
|
+
? ((chart.getOption() as ChartsOption | undefined)?.dataZoom as
|
|
283
|
+
| Array<{ start?: number; end?: number }>
|
|
284
|
+
| undefined)
|
|
285
|
+
: undefined;
|
|
286
|
+
prevXAxisKeyRef.current = xAxisKey;
|
|
269
287
|
chart.setOption(chartOption as ChartOptionType, true);
|
|
288
|
+
const nextZooms = (chartOption as ChartsOption).dataZoom;
|
|
289
|
+
if (
|
|
290
|
+
prevZoomRanges?.length &&
|
|
291
|
+
Array.isArray(nextZooms) &&
|
|
292
|
+
nextZooms.length === prevZoomRanges.length
|
|
293
|
+
) {
|
|
294
|
+
chart.setOption({
|
|
295
|
+
dataZoom: prevZoomRanges.map((z) => ({ start: z.start, end: z.end })),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 向调用方同步当前可见窗口(初始渲染与恢复后),用于按展示部分重算坐标轴
|
|
300
|
+
if (onDataZoomWindowChanged) {
|
|
301
|
+
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
302
|
+
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
303
|
+
const zoom = zooms?.[0];
|
|
304
|
+
const totalLen = xAxisData?.length ?? 0;
|
|
305
|
+
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
306
|
+
onDataZoomWindowChanged(
|
|
307
|
+
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
308
|
+
);
|
|
309
|
+
} else {
|
|
310
|
+
// 无 dataZoom(如数据量不足未启用滚动)时窗口即全量,同步一次避免残留旧窗口
|
|
311
|
+
onDataZoomWindowChanged({
|
|
312
|
+
startIndex: 0,
|
|
313
|
+
endIndex: Math.max(0, totalLen - 1),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
270
317
|
|
|
271
318
|
// 添加 resize 监听
|
|
272
319
|
window.addEventListener("resize", handleResize);
|
|
@@ -282,6 +329,59 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
282
329
|
chartInstanceRef.current?.on("click", onClick);
|
|
283
330
|
}
|
|
284
331
|
|
|
332
|
+
// 图例点选事件
|
|
333
|
+
const handleLegendSelectChanged = (params: unknown) => {
|
|
334
|
+
onLegendSelectChanged?.(
|
|
335
|
+
(params as { selected: Record<string, boolean> }).selected,
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
if (onLegendSelectChanged) {
|
|
339
|
+
chartInstanceRef.current?.on(
|
|
340
|
+
"legendselectchanged",
|
|
341
|
+
handleLegendSelectChanged,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// dataZoom 原生交互(slider 拖动 / inside 缩放)→ 同步可见窗口
|
|
346
|
+
const handleDataZoom = (params: unknown) => {
|
|
347
|
+
const raw = params as {
|
|
348
|
+
start?: number;
|
|
349
|
+
end?: number;
|
|
350
|
+
startValue?: number;
|
|
351
|
+
endValue?: number;
|
|
352
|
+
batch?: Array<{
|
|
353
|
+
start?: number;
|
|
354
|
+
end?: number;
|
|
355
|
+
startValue?: number;
|
|
356
|
+
endValue?: number;
|
|
357
|
+
}>;
|
|
358
|
+
};
|
|
359
|
+
const info = raw?.batch?.[0] ?? raw;
|
|
360
|
+
if (
|
|
361
|
+
typeof info?.startValue === "number" &&
|
|
362
|
+
typeof info?.endValue === "number"
|
|
363
|
+
) {
|
|
364
|
+
onDataZoomWindowChanged?.({
|
|
365
|
+
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
366
|
+
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
367
|
+
});
|
|
368
|
+
} else if (
|
|
369
|
+
typeof info?.start === "number" &&
|
|
370
|
+
typeof info?.end === "number"
|
|
371
|
+
) {
|
|
372
|
+
onDataZoomWindowChanged?.(
|
|
373
|
+
percentWindowToIndexWindow(
|
|
374
|
+
info.start,
|
|
375
|
+
info.end,
|
|
376
|
+
xAxisData?.length ?? 0,
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
if (onDataZoomWindowChanged) {
|
|
382
|
+
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
383
|
+
}
|
|
384
|
+
|
|
285
385
|
// 图例自动滚动(与 Pie/Bar 一致)
|
|
286
386
|
const opt = chartOption as ChartsOption;
|
|
287
387
|
const legendDataLen = Array.isArray(opt.legend)
|
|
@@ -314,6 +414,17 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
314
414
|
chartInstanceRef.current?.off("click", onClick);
|
|
315
415
|
}
|
|
316
416
|
|
|
417
|
+
if (onLegendSelectChanged) {
|
|
418
|
+
chartInstanceRef.current?.off(
|
|
419
|
+
"legendselectchanged",
|
|
420
|
+
handleLegendSelectChanged,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (onDataZoomWindowChanged) {
|
|
425
|
+
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
426
|
+
}
|
|
427
|
+
|
|
317
428
|
if (legendScrollRef.current) {
|
|
318
429
|
legendScrollRef.current.dispose();
|
|
319
430
|
legendScrollRef.current = null;
|
|
@@ -323,6 +434,9 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
323
434
|
chartOption,
|
|
324
435
|
handleResize,
|
|
325
436
|
onClick,
|
|
437
|
+
onLegendSelectChanged,
|
|
438
|
+
onDataZoomWindowChanged,
|
|
439
|
+
xAxisData,
|
|
326
440
|
enableLegendAutoScroll,
|
|
327
441
|
legendVisibleCount,
|
|
328
442
|
legendScrollInterval,
|
|
@@ -355,10 +469,26 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
355
469
|
startIndex: normalizedScroll.startIndex,
|
|
356
470
|
autoPlay: normalizedScroll.autoScroll,
|
|
357
471
|
enableWheelScroll,
|
|
472
|
+
onWindowChange: onDataZoomWindowChanged
|
|
473
|
+
? (startPercent, endPercent) =>
|
|
474
|
+
onDataZoomWindowChanged(
|
|
475
|
+
percentWindowToIndexWindow(
|
|
476
|
+
startPercent,
|
|
477
|
+
endPercent,
|
|
478
|
+
xAxisData.length,
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
: undefined,
|
|
358
482
|
});
|
|
359
483
|
|
|
360
484
|
return dispose;
|
|
361
|
-
}, [
|
|
485
|
+
}, [
|
|
486
|
+
dataZoom,
|
|
487
|
+
normalizedScroll,
|
|
488
|
+
sliderDataZoom,
|
|
489
|
+
xAxisData,
|
|
490
|
+
onDataZoomWindowChanged,
|
|
491
|
+
]);
|
|
362
492
|
|
|
363
493
|
// 组件卸载时清理资源
|
|
364
494
|
useEffect(() => {
|