@hsu-react/ui 0.0.8 → 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.
Files changed (53) hide show
  1. package/es/components/Chart/Bar/index.d.ts +5 -1
  2. package/es/components/Chart/Bar/index.js +84 -9
  3. package/es/components/Chart/Line/index.d.ts +5 -1
  4. package/es/components/Chart/Line/index.js +84 -9
  5. package/es/components/Chart/_utils/autoSmooth.d.ts +2 -0
  6. package/es/components/Chart/_utils/autoSmooth.js +12 -2
  7. package/es/components/Chart/_utils/cartesian.d.ts +11 -0
  8. package/es/components/Chart/_utils/cartesian.js +19 -0
  9. package/es/components/Chart/chartUtils/chartTooltipFormatter.d.ts +2 -0
  10. package/es/components/Chart/chartUtils/chartTooltipFormatter.js +13 -3
  11. package/es/components/Chart/chartUtils/index.d.ts +5 -2
  12. package/es/components/Chart/chartUtils/index.js +3 -1
  13. package/es/components/Chart/chartUtils/useDataZoomWindow.d.ts +19 -0
  14. package/es/components/Chart/chartUtils/useDataZoomWindow.js +37 -0
  15. package/es/components/Chart/chartUtils/useLegendSelection.d.ts +14 -0
  16. package/es/components/Chart/chartUtils/useLegendSelection.js +35 -0
  17. package/es/components/Modal/index.module.scss +1 -0
  18. package/es/components/Panel/ListPanel/ListModalPanel/index.js +33 -5
  19. package/es/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
  20. package/es/components/Panel/ListPanel/index.module.scss +11 -12
  21. package/lib/components/Chart/Bar/index.d.ts +5 -1
  22. package/lib/components/Chart/Bar/index.js +68 -4
  23. package/lib/components/Chart/Line/index.d.ts +5 -1
  24. package/lib/components/Chart/Line/index.js +68 -4
  25. package/lib/components/Chart/_utils/autoSmooth.d.ts +2 -0
  26. package/lib/components/Chart/_utils/autoSmooth.js +11 -1
  27. package/lib/components/Chart/_utils/cartesian.d.ts +11 -0
  28. package/lib/components/Chart/_utils/cartesian.js +21 -1
  29. package/lib/components/Chart/chartUtils/chartTooltipFormatter.d.ts +2 -0
  30. package/lib/components/Chart/chartUtils/chartTooltipFormatter.js +5 -3
  31. package/lib/components/Chart/chartUtils/index.d.ts +5 -2
  32. package/lib/components/Chart/chartUtils/index.js +14 -0
  33. package/lib/components/Chart/chartUtils/useDataZoomWindow.d.ts +19 -0
  34. package/lib/components/Chart/chartUtils/useDataZoomWindow.js +31 -0
  35. package/lib/components/Chart/chartUtils/useLegendSelection.d.ts +14 -0
  36. package/lib/components/Chart/chartUtils/useLegendSelection.js +26 -0
  37. package/lib/components/Modal/index.module.scss +1 -0
  38. package/lib/components/Panel/ListPanel/ListModalPanel/index.js +21 -2
  39. package/lib/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
  40. package/lib/components/Panel/ListPanel/index.module.scss +11 -12
  41. package/package.json +1 -1
  42. package/src/components/Chart/Bar/index.tsx +132 -2
  43. package/src/components/Chart/Line/index.tsx +132 -2
  44. package/src/components/Chart/_utils/autoSmooth.ts +181 -164
  45. package/src/components/Chart/_utils/cartesian.ts +28 -0
  46. package/src/components/Chart/chartUtils/chartTooltipFormatter.ts +11 -2
  47. package/src/components/Chart/chartUtils/index.ts +53 -37
  48. package/src/components/Chart/chartUtils/useDataZoomWindow.ts +61 -0
  49. package/src/components/Chart/chartUtils/useLegendSelection.ts +35 -0
  50. package/src/components/Modal/index.module.scss +1 -0
  51. package/src/components/Panel/ListPanel/ListModalPanel/index.module.scss +17 -2
  52. package/src/components/Panel/ListPanel/ListModalPanel/index.tsx +261 -233
  53. 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
+ }
@@ -62,6 +62,7 @@
62
62
  align-items: center;
63
63
  justify-content: flex-end;
64
64
  column-gap: 10px;
65
+ margin-left: auto; // title 为空时仍保持靠右
65
66
  }
66
67
  }
67
68
  }
@@ -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: max-content;
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: 14px 16px;
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
- padding: 10px;
107
+ // 页面级页签:无底色容器,页签拆成独立圆角块、块间留空;
108
+ // margin/radius 需 !important 压过 TabBar 内部的连体分段样式(负 margin + 首尾切角)
109
+ .headerTabBar {
110
+ column-gap: 4px;
107
111
 
108
- background: var(--cf-subtle);
112
+ > div {
113
+ margin-left: 0 !important;
114
+ border-radius: 6px !important;
109
115
 
110
- border-radius: 8px 8px 0px 0px;
111
- border: 1px solid var(--cf-border);
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.8",
3
+ "version": "0.0.9",
4
4
  "description": "一套基于 React + Ant Design 的中后台业务组件库",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData]);
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
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData]);
485
+ }, [
486
+ dataZoom,
487
+ normalizedScroll,
488
+ sliderDataZoom,
489
+ xAxisData,
490
+ onDataZoomWindowChanged,
491
+ ]);
362
492
 
363
493
  // 组件卸载时清理资源
364
494
  useEffect(() => {