@hsu-react/ui 2.5.3 → 2.5.4
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/Bubble/index.js +7 -2
- package/es/components/Chart/Common/index.js +7 -2
- package/es/components/Chart/Heatmap/index.js +7 -2
- package/es/components/Chart/Pie/Pie3D/index.js +8 -2
- package/es/components/Chart/Pie/index.js +7 -2
- package/es/components/Chat/ChatInput/index.js +7 -2
- package/es/components/Icon/index.js +8 -2
- package/es/components/Input/Number/index.js +28 -21
- package/es/components/Input/Password/index.js +44 -36
- package/es/components/Input/Search/index.js +44 -36
- package/es/components/Input/TextArea/index.js +64 -42
- package/es/components/Input/index.js +40 -34
- package/es/components/Select/_hooks/useSelectInputOptions.js +8 -2
- package/es/components/Slider/index.js +24 -15
- package/es/components/Upload/_hooks/useUploadFileList.js +8 -2
- package/es/components/Upload/_hooks/useUploadOperations.js +8 -2
- package/es/hooks/useLatestRef.d.ts +24 -0
- package/es/hooks/useLatestRef.js +29 -0
- package/lib/components/Chart/Bubble/index.js +6 -2
- package/lib/components/Chart/Common/index.js +6 -2
- package/lib/components/Chart/Heatmap/index.js +6 -2
- package/lib/components/Chart/Pie/Pie3D/index.js +7 -2
- package/lib/components/Chart/Pie/index.js +6 -2
- package/lib/components/Chat/ChatInput/index.js +6 -2
- package/lib/components/Icon/index.js +7 -2
- package/lib/components/Input/Number/index.js +27 -18
- package/lib/components/Input/Password/index.js +40 -26
- package/lib/components/Input/Search/index.js +42 -26
- package/lib/components/Input/TextArea/index.js +54 -26
- package/lib/components/Input/index.js +37 -24
- package/lib/components/Select/_hooks/useSelectInputOptions.js +7 -2
- package/lib/components/Slider/index.js +23 -11
- package/lib/components/Upload/_hooks/useUploadFileList.js +7 -2
- package/lib/components/Upload/_hooks/useUploadOperations.js +7 -2
- package/lib/hooks/useLatestRef.d.ts +24 -0
- package/lib/hooks/useLatestRef.js +35 -0
- package/package.json +9 -3
- package/src/__tests__/setup.ts +26 -0
- package/src/components/Chart/Bubble/index.tsx +7 -2
- package/src/components/Chart/Common/index.tsx +7 -2
- package/src/components/Chart/Heatmap/index.tsx +7 -2
- package/src/components/Chart/Pie/Pie3D/index.tsx +7 -2
- package/src/components/Chart/Pie/index.tsx +7 -2
- package/src/components/Chat/ChatInput/index.tsx +7 -2
- package/src/components/Icon/index.tsx +7 -2
- package/src/components/Input/Number/index.tsx +25 -23
- package/src/components/Input/Password/index.tsx +37 -26
- package/src/components/Input/Search/index.tsx +39 -26
- package/src/components/Input/TextArea/index.test.tsx +208 -0
- package/src/components/Input/TextArea/index.tsx +56 -30
- package/src/components/Input/index.test.tsx +189 -0
- package/src/components/Input/index.tsx +36 -29
- package/src/components/Select/_hooks/useSelectInputOptions.ts +6 -9
- package/src/components/Slider/index.tsx +22 -12
- package/src/components/Upload/_hooks/useUploadFileList.ts +6 -2
- package/src/components/Upload/_hooks/useUploadOperations.ts +7 -2
- package/src/hooks/useLatestRef.ts +31 -0
|
@@ -21,25 +21,37 @@ const Slider = props => {
|
|
|
21
21
|
...sliderConfig
|
|
22
22
|
} = props;
|
|
23
23
|
const [_value, setValue] = (0, _react.useState)(value);
|
|
24
|
-
|
|
25
|
-
(0, _react.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
// 最近一次通知出去的值,代替原来那个用 state 记账的写法
|
|
25
|
+
const notifiedRef = (0, _react.useRef)(value);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `onChange` 是「用户拖了滑块」这个**事件**的通知,不是从 state 推导出来的结果,
|
|
29
|
+
* 所以在事件处理里发,不在 effect 里发。详见 `Input/TextArea/index.tsx` 里那段
|
|
30
|
+
* 说明——旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
|
|
31
|
+
* 会让传内联箭头的消费方死循环。
|
|
32
|
+
*/
|
|
33
|
+
const handleChange = next => {
|
|
34
|
+
setValue(next);
|
|
35
|
+
if (next === notifiedRef.current) return;
|
|
36
|
+
notifiedRef.current = next;
|
|
37
|
+
onChange?.(next);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 外部值(`value` / `topValue`)变了才回写,不跟着通知走
|
|
31
41
|
(0, _react.useEffect)(() => {
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const next = topValue || value;
|
|
43
|
+
if (next !== notifiedRef.current) {
|
|
44
|
+
notifiedRef.current = next;
|
|
45
|
+
setValue(next);
|
|
34
46
|
}
|
|
35
|
-
}, [
|
|
47
|
+
}, [topValue, value]);
|
|
36
48
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.Slider, {
|
|
37
49
|
className: (0, _classnames.default)([_indexModule.default.slider, className]),
|
|
38
50
|
tooltip: {
|
|
39
51
|
open: false
|
|
40
52
|
},
|
|
41
53
|
value: _value,
|
|
42
|
-
onChange:
|
|
54
|
+
onChange: handleChange,
|
|
43
55
|
...sliderConfig
|
|
44
56
|
});
|
|
45
57
|
};
|
|
@@ -7,6 +7,7 @@ exports.useUploadFileList = useUploadFileList;
|
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _hsuUtils = require("hsu-utils");
|
|
9
9
|
var _utils = require("../_utils");
|
|
10
|
+
var _useLatestRef = require("../../../hooks/useLatestRef");
|
|
10
11
|
/**
|
|
11
12
|
* Manage the upload file list state
|
|
12
13
|
*/
|
|
@@ -17,6 +18,10 @@ function useUploadFileList({
|
|
|
17
18
|
}) {
|
|
18
19
|
const [_fileList, setFilelist] = (0, _react.useState)([]);
|
|
19
20
|
const [lastFileList, setLastFileList] = (0, _react.useState)([]);
|
|
21
|
+
const onChangeRef = (0, _useLatestRef.useLatestRef)(onChange);
|
|
22
|
+
|
|
23
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
24
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
20
25
|
(0, _react.useEffect)(() => {
|
|
21
26
|
if (rmFile) {
|
|
22
27
|
const file = _fileList.find(item => item.uid === rmFile);
|
|
@@ -24,14 +29,14 @@ function useUploadFileList({
|
|
|
24
29
|
if (file) {
|
|
25
30
|
setFilelist(filteredList);
|
|
26
31
|
setTimeout(() => {
|
|
27
|
-
|
|
32
|
+
onChangeRef.current?.({
|
|
28
33
|
file,
|
|
29
34
|
fileList: filteredList
|
|
30
35
|
});
|
|
31
36
|
}, 100);
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
}, [_fileList,
|
|
39
|
+
}, [_fileList, onChangeRef, rmFile]);
|
|
35
40
|
(0, _react.useEffect)(() => {
|
|
36
41
|
if (!_hsuUtils.Equal.ObjEqual(fileList ?? [], lastFileList)) {
|
|
37
42
|
if (fileList?.length) {
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.useUploadOperations = useUploadOperations;
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _hsuUtils = require("hsu-utils");
|
|
9
|
+
var _useLatestRef = require("../../../hooks/useLatestRef");
|
|
9
10
|
/**
|
|
10
11
|
* Manage upload and download operations
|
|
11
12
|
*/
|
|
@@ -19,6 +20,7 @@ function useUploadOperations({
|
|
|
19
20
|
downloading: {},
|
|
20
21
|
uploadingList: {}
|
|
21
22
|
});
|
|
23
|
+
const onUploadingListRef = (0, _useLatestRef.useLatestRef)(onUploadingList);
|
|
22
24
|
|
|
23
25
|
// Sync to the ref on update
|
|
24
26
|
(0, _react.useEffect)(() => {
|
|
@@ -27,12 +29,15 @@ function useUploadOperations({
|
|
|
27
29
|
(0, _react.useEffect)(() => {
|
|
28
30
|
operationsRef.current.uploadingList = uploadingList;
|
|
29
31
|
}, [uploadingList]);
|
|
32
|
+
|
|
33
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
34
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
30
35
|
(0, _react.useEffect)(() => {
|
|
31
36
|
if (!_hsuUtils.Equal.ObjEqual(uploadingList, lastUploadList)) {
|
|
32
|
-
|
|
37
|
+
onUploadingListRef.current?.((0, _hsuUtils.deepCopy)(uploadingList));
|
|
33
38
|
setLastUploadList((0, _hsuUtils.deepCopy)(uploadingList));
|
|
34
39
|
}
|
|
35
|
-
}, [uploadingList, lastUploadList,
|
|
40
|
+
}, [uploadingList, lastUploadList, onUploadingListRef]);
|
|
36
41
|
|
|
37
42
|
// Cleanup function
|
|
38
43
|
(0, _react.useEffect)(() => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* 把一个每次渲染都可能换引用的值(通常是回调 prop)装进一个**引用恒定**的 ref。
|
|
4
|
+
*
|
|
5
|
+
* 用途只有一个:让 effect 不必把回调放进依赖数组。
|
|
6
|
+
* 回调放进依赖数组、effect 体内又调它,是一条会让消费方死循环的写法——
|
|
7
|
+
* 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
|
|
8
|
+
* 每次渲染都是新引用 → effect 重跑 → 调回调 → 父级 setState → 再渲染 →
|
|
9
|
+
* 又是新引用 …… 直到 React 抛 `Maximum update depth exceeded`。
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const onDoneRef = useLatestRef(onDone);
|
|
13
|
+
* useEffect(() => {
|
|
14
|
+
* const t = setTimeout(() => onDoneRef.current?.(), 1000);
|
|
15
|
+
* return () => clearTimeout(t);
|
|
16
|
+
* }, [onDoneRef]); // 只在挂载时跑一次,且永远调到最新的 onDone
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* 注意:渲染期间就地赋值。React 官方对 ref 的约束是「不要在渲染中**读**可变值」,
|
|
20
|
+
* 写入最新的 props 是 useEffectEvent 落地前的通行做法(ahooks 的 `useLatest`、
|
|
21
|
+
* React 文档里的 `useEvent` polyfill 都是这么写的)。
|
|
22
|
+
*/
|
|
23
|
+
export declare function useLatestRef<T>(value: T): import("react").MutableRefObject<T>;
|
|
24
|
+
export default useLatestRef;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
exports.useLatestRef = useLatestRef;
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
/**
|
|
10
|
+
* 把一个每次渲染都可能换引用的值(通常是回调 prop)装进一个**引用恒定**的 ref。
|
|
11
|
+
*
|
|
12
|
+
* 用途只有一个:让 effect 不必把回调放进依赖数组。
|
|
13
|
+
* 回调放进依赖数组、effect 体内又调它,是一条会让消费方死循环的写法——
|
|
14
|
+
* 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
|
|
15
|
+
* 每次渲染都是新引用 → effect 重跑 → 调回调 → 父级 setState → 再渲染 →
|
|
16
|
+
* 又是新引用 …… 直到 React 抛 `Maximum update depth exceeded`。
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const onDoneRef = useLatestRef(onDone);
|
|
20
|
+
* useEffect(() => {
|
|
21
|
+
* const t = setTimeout(() => onDoneRef.current?.(), 1000);
|
|
22
|
+
* return () => clearTimeout(t);
|
|
23
|
+
* }, [onDoneRef]); // 只在挂载时跑一次,且永远调到最新的 onDone
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* 注意:渲染期间就地赋值。React 官方对 ref 的约束是「不要在渲染中**读**可变值」,
|
|
27
|
+
* 写入最新的 props 是 useEffectEvent 落地前的通行做法(ahooks 的 `useLatest`、
|
|
28
|
+
* React 文档里的 `useEvent` polyfill 都是这么写的)。
|
|
29
|
+
*/
|
|
30
|
+
function useLatestRef(value) {
|
|
31
|
+
const ref = (0, _react.useRef)(value);
|
|
32
|
+
ref.current = value;
|
|
33
|
+
return ref;
|
|
34
|
+
}
|
|
35
|
+
var _default = exports.default = useLatestRef;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hsu-react/ui",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"description": "一套基于 React + Ant Design 的中后台业务组件库",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"docs:build": "dumi build",
|
|
43
43
|
"docs:preview": "dumi preview",
|
|
44
44
|
"build": "node scripts/clean-dist.cjs && node scripts/gen-tokens-scss.cjs && father build && node scripts/mark-cjs.cjs && node scripts/check-heavy-deps.cjs",
|
|
45
|
-
"prepublishOnly": "node scripts/clean-dist.cjs && node scripts/gen-tokens-scss.cjs --check && father build && node scripts/mark-cjs.cjs && node scripts/check-heavy-deps.cjs",
|
|
45
|
+
"prepublishOnly": "node scripts/clean-dist.cjs && node scripts/gen-tokens-scss.cjs --check && vitest run && father build && node scripts/mark-cjs.cjs && node scripts/check-heavy-deps.cjs",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
46
48
|
"check:deps": "node scripts/check-heavy-deps.cjs",
|
|
47
49
|
"tokens": "node scripts/gen-tokens-scss.cjs",
|
|
48
50
|
"check:tokens": "node scripts/gen-tokens-scss.cjs --check"
|
|
@@ -115,6 +117,8 @@
|
|
|
115
117
|
},
|
|
116
118
|
"devDependencies": {
|
|
117
119
|
"@ant-design/icons": "^6.3.2",
|
|
120
|
+
"@testing-library/dom": "^10",
|
|
121
|
+
"@testing-library/react": "^16",
|
|
118
122
|
"@types/js-cookie": "^3.0.6",
|
|
119
123
|
"@types/lodash": "^4.17.4",
|
|
120
124
|
"@types/markdown-it": "^14.1.2",
|
|
@@ -126,6 +130,7 @@
|
|
|
126
130
|
"antd": "6.5.4",
|
|
127
131
|
"dumi": "^2.4.0",
|
|
128
132
|
"father": "^4.5.0",
|
|
133
|
+
"jsdom": "^25",
|
|
129
134
|
"mobx": "^6.12.3",
|
|
130
135
|
"mobx-react-lite": "^4.0.7",
|
|
131
136
|
"react": "^18.3.1",
|
|
@@ -134,7 +139,8 @@
|
|
|
134
139
|
"react-router": "^6.23.1",
|
|
135
140
|
"react-router-dom": "^6.23.1",
|
|
136
141
|
"sass": "^1.101.0",
|
|
137
|
-
"typescript": "^5.4.5"
|
|
142
|
+
"typescript": "^5.4.5",
|
|
143
|
+
"vitest": "^2"
|
|
138
144
|
},
|
|
139
145
|
"peerDependenciesMeta": {
|
|
140
146
|
"react-router": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsdom 没有 `matchMedia` / `ResizeObserver`,antd 与 rc-* 会直接调它们。
|
|
3
|
+
* 这里补齐,缺一个就整个测试文件起不来
|
|
4
|
+
*/
|
|
5
|
+
import "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
if (!window.matchMedia) {
|
|
8
|
+
window.matchMedia = ((query: string) => ({
|
|
9
|
+
matches: false,
|
|
10
|
+
media: query,
|
|
11
|
+
onchange: null,
|
|
12
|
+
addListener: () => {},
|
|
13
|
+
removeListener: () => {},
|
|
14
|
+
addEventListener: () => {},
|
|
15
|
+
removeEventListener: () => {},
|
|
16
|
+
dispatchEvent: () => false,
|
|
17
|
+
})) as unknown as typeof window.matchMedia;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!(globalThis as { ResizeObserver?: unknown }).ResizeObserver) {
|
|
21
|
+
(globalThis as { ResizeObserver?: unknown }).ResizeObserver = class {
|
|
22
|
+
observe() {}
|
|
23
|
+
unobserve() {}
|
|
24
|
+
disconnect() {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
defaultBubbleColorList,
|
|
13
13
|
drawCircles,
|
|
14
14
|
} from "../_utils/bubble";
|
|
15
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
15
16
|
|
|
16
17
|
export interface BubbleDataItem {
|
|
17
18
|
name: string;
|
|
@@ -370,6 +371,10 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
370
371
|
}
|
|
371
372
|
}, [chartOption, data]);
|
|
372
373
|
|
|
374
|
+
const onChartRef = useLatestRef(onChart);
|
|
375
|
+
|
|
376
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
377
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
373
378
|
// Initialize the chart
|
|
374
379
|
useEffect(() => {
|
|
375
380
|
if (!chartRef.current) return;
|
|
@@ -382,7 +387,7 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
382
387
|
chart = echarts.init(chartRef.current);
|
|
383
388
|
chartInstanceRef.current = chart;
|
|
384
389
|
// Call the onChart callback on first initialization
|
|
385
|
-
|
|
390
|
+
onChartRef.current?.(chart);
|
|
386
391
|
}
|
|
387
392
|
|
|
388
393
|
// Resize first so echarts picks up the correct container size, then apply the option
|
|
@@ -410,7 +415,7 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
410
415
|
containerSize.width,
|
|
411
416
|
containerSize.height,
|
|
412
417
|
handleResize,
|
|
413
|
-
|
|
418
|
+
onChartRef,
|
|
414
419
|
onClick,
|
|
415
420
|
]);
|
|
416
421
|
|
|
@@ -3,6 +3,7 @@ import useContainerReady from "../_hooks/useContainerReady";
|
|
|
3
3
|
import styles from "../index.module.scss";
|
|
4
4
|
import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
|
|
5
5
|
import * as echarts from "echarts";
|
|
6
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
6
7
|
|
|
7
8
|
const Common: React.FC<ChartCommonProps> = (props) => {
|
|
8
9
|
const { className, style, onChart, ...coreOption } = props;
|
|
@@ -31,6 +32,10 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
31
32
|
chartInstanceRef.current?.resize();
|
|
32
33
|
}, []);
|
|
33
34
|
|
|
35
|
+
const onChartRef = useLatestRef(onChart);
|
|
36
|
+
|
|
37
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
38
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
34
39
|
// Initialize the chart
|
|
35
40
|
useEffect(() => {
|
|
36
41
|
if (!chartRef.current || !containerReady) return;
|
|
@@ -46,7 +51,7 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
46
51
|
chart.setOption(chartOption as ChartOptionType, true);
|
|
47
52
|
|
|
48
53
|
// Chart-ready callback (can be used for legend auto-scroll, etc.)
|
|
49
|
-
|
|
54
|
+
onChartRef.current?.(chart);
|
|
50
55
|
|
|
51
56
|
// Add resize listener
|
|
52
57
|
window.addEventListener("resize", handleResize);
|
|
@@ -61,7 +66,7 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
61
66
|
return () => {
|
|
62
67
|
window.removeEventListener("resize", handleResize);
|
|
63
68
|
};
|
|
64
|
-
}, [chartOption, handleResize,
|
|
69
|
+
}, [chartOption, handleResize, onChartRef,
|
|
65
70
|
containerReady,
|
|
66
71
|
]);
|
|
67
72
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../_utils/heatmap";
|
|
10
10
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
11
11
|
import useIsDark from "../../../hooks/useIsDark";
|
|
12
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
12
13
|
|
|
13
14
|
export interface HeatmapDataItem {
|
|
14
15
|
/** X-axis index or name */
|
|
@@ -266,6 +267,10 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
266
267
|
chartInstanceRef.current?.resize();
|
|
267
268
|
}, []);
|
|
268
269
|
|
|
270
|
+
const onChartRef = useLatestRef(onChart);
|
|
271
|
+
|
|
272
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
273
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
269
274
|
// Initialize the chart
|
|
270
275
|
useEffect(() => {
|
|
271
276
|
if (!chartRef.current || !containerReady) return;
|
|
@@ -276,7 +281,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
276
281
|
chart = echarts.init(chartRef.current);
|
|
277
282
|
chartInstanceRef.current = chart;
|
|
278
283
|
// Call the onChart callback on first initialization
|
|
279
|
-
|
|
284
|
+
onChartRef.current?.(chart);
|
|
280
285
|
}
|
|
281
286
|
|
|
282
287
|
// Apply the option
|
|
@@ -304,7 +309,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
304
309
|
chartInstanceRef.current?.off("click", onClick);
|
|
305
310
|
}
|
|
306
311
|
};
|
|
307
|
-
}, [chartOption, handleResize,
|
|
312
|
+
}, [chartOption, handleResize, onChartRef, onClick,
|
|
308
313
|
containerReady,
|
|
309
314
|
]);
|
|
310
315
|
|
|
@@ -7,6 +7,7 @@ import { ChartCommonProps, ChartOptionType, ChartsOption } from "../..";
|
|
|
7
7
|
import { useMouseOverHandler, useGlobalOutHandler } from "./_hooks";
|
|
8
8
|
import { getPie3DOption } from "./_utils/option";
|
|
9
9
|
import styles from "../../index.module.scss";
|
|
10
|
+
import { useLatestRef } from "../../../../hooks/useLatestRef";
|
|
10
11
|
|
|
11
12
|
export interface Pie3DDataItem {
|
|
12
13
|
name: string;
|
|
@@ -170,6 +171,10 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
170
171
|
{ minHeight, maxHeight, yOffset, autoRotate }
|
|
171
172
|
);
|
|
172
173
|
|
|
174
|
+
const onChartRef = useLatestRef(onChart);
|
|
175
|
+
|
|
176
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
177
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
173
178
|
useEffect(() => {
|
|
174
179
|
if (!chartRef.current || !containerReady) return;
|
|
175
180
|
|
|
@@ -177,7 +182,7 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
177
182
|
if (!chart) {
|
|
178
183
|
chart = echarts.init(chartRef.current);
|
|
179
184
|
chartInstanceRef.current = chart;
|
|
180
|
-
|
|
185
|
+
onChartRef.current?.(chart);
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
chart.setOption(chartOption as ChartOptionType, true);
|
|
@@ -211,7 +216,7 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
211
216
|
handleResize,
|
|
212
217
|
handleMouseOver,
|
|
213
218
|
handleGlobalOut,
|
|
214
|
-
|
|
219
|
+
onChartRef,
|
|
215
220
|
onClick,
|
|
216
221
|
containerReady,
|
|
217
222
|
]);
|
|
@@ -13,6 +13,7 @@ import { autoScrollLegend } from "../chartUtils";
|
|
|
13
13
|
|
|
14
14
|
import styles from "../index.module.scss";
|
|
15
15
|
import ChartPie3D, { ChartPie3DProps } from "./Pie3D";
|
|
16
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
16
17
|
|
|
17
18
|
export interface ChartPieProps extends ChartCommonProps {
|
|
18
19
|
chartTitle?: string;
|
|
@@ -252,6 +253,10 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
252
253
|
chartInstanceRef.current?.resize();
|
|
253
254
|
}, []);
|
|
254
255
|
|
|
256
|
+
const onChartRef = useLatestRef(onChart);
|
|
257
|
+
|
|
258
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
259
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
255
260
|
// Initialize the chart
|
|
256
261
|
useEffect(() => {
|
|
257
262
|
if (!chartRef.current || !containerReady) return;
|
|
@@ -262,7 +267,7 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
262
267
|
chart = echarts.init(chartRef.current);
|
|
263
268
|
chartInstanceRef.current = chart;
|
|
264
269
|
// Call the onChart callback on first initialization
|
|
265
|
-
|
|
270
|
+
onChartRef.current?.(chart);
|
|
266
271
|
}
|
|
267
272
|
|
|
268
273
|
// Apply the option
|
|
@@ -315,7 +320,7 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
315
320
|
}, [
|
|
316
321
|
chartOption,
|
|
317
322
|
handleResize,
|
|
318
|
-
|
|
323
|
+
onChartRef,
|
|
319
324
|
onClick,
|
|
320
325
|
enableLegendAutoScroll,
|
|
321
326
|
seriesData,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./_components";
|
|
15
15
|
import classNames from "classnames";
|
|
16
16
|
import Button, { ButtonProps } from "../../Button";
|
|
17
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
17
18
|
export type { AgentToggleConfig } from "./_components";
|
|
18
19
|
|
|
19
20
|
export interface AgentConfig {
|
|
@@ -127,6 +128,10 @@ const ChatInput: React.FC<ChatInputProps> = (props) => {
|
|
|
127
128
|
// Use external state or internal state
|
|
128
129
|
const agentsState = isControlled ? externalAgentsState : internalAgentsState;
|
|
129
130
|
|
|
131
|
+
const onAgentsChangeRef = useLatestRef(onAgentsChange);
|
|
132
|
+
|
|
133
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
134
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
130
135
|
// When the model switches, sync the agents state (only in uncontrolled mode)
|
|
131
136
|
useEffect(() => {
|
|
132
137
|
if (isControlled) return; // In controlled mode, state is managed externally
|
|
@@ -136,12 +141,12 @@ const ChatInput: React.FC<ChatInputProps> = (props) => {
|
|
|
136
141
|
);
|
|
137
142
|
if (currentModel?.agents && currentModel.agents.length > 0) {
|
|
138
143
|
setInternalAgentsState(currentModel.agents);
|
|
139
|
-
|
|
144
|
+
onAgentsChangeRef.current?.(currentModel.agents);
|
|
140
145
|
}
|
|
141
146
|
}, [
|
|
142
147
|
modelConfig?.modelType,
|
|
143
148
|
modelConfig?.modelList,
|
|
144
|
-
|
|
149
|
+
onAgentsChangeRef,
|
|
145
150
|
isControlled,
|
|
146
151
|
]);
|
|
147
152
|
|
|
@@ -8,6 +8,7 @@ import * as AntdIcons from "@ant-design/icons";
|
|
|
8
8
|
import React, { useCallback, useEffect, useRef } from "react";
|
|
9
9
|
import classNames from "classnames";
|
|
10
10
|
import styles from "./index.module.scss";
|
|
11
|
+
import { useLatestRef } from "../../hooks/useLatestRef";
|
|
11
12
|
|
|
12
13
|
type AntdNamedIconComponent = React.ForwardRefExoticComponent<
|
|
13
14
|
{
|
|
@@ -73,9 +74,13 @@ const Icon = React.forwardRef<HTMLSpanElement, IconProps>((props, forwardedRef)
|
|
|
73
74
|
[forwardedRef]
|
|
74
75
|
);
|
|
75
76
|
|
|
77
|
+
const onRefRef = useLatestRef(onRef);
|
|
78
|
+
|
|
79
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
80
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
76
81
|
useEffect(() => {
|
|
77
|
-
|
|
78
|
-
}, [
|
|
82
|
+
onRefRef.current?.(ref);
|
|
83
|
+
}, [onRefRef, ref]);
|
|
79
84
|
|
|
80
85
|
const mergedStyle: React.CSSProperties = {
|
|
81
86
|
color,
|
|
@@ -8,8 +8,8 @@ import { CloseCircleFilled } from "@ant-design/icons";
|
|
|
8
8
|
|
|
9
9
|
import classNames from "classnames";
|
|
10
10
|
import styles from "./index.module.scss";
|
|
11
|
-
import { useDebounceEffect } from "ahooks";
|
|
12
11
|
import type { InputNumberRef } from "../../../types/antd";
|
|
12
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
13
13
|
|
|
14
14
|
export interface InputNumberProps extends Omit<
|
|
15
15
|
AntdInputNumberProps,
|
|
@@ -44,6 +44,7 @@ const InputNumber: React.FC<InputNumberProps> = (props) => {
|
|
|
44
44
|
// `getRef` keeps handing consumers an `HTMLInputElement` exactly as before.
|
|
45
45
|
const ref = useRef<InputNumberRef>(null);
|
|
46
46
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const getRefRef = useLatestRef(getRef);
|
|
47
48
|
|
|
48
49
|
// On initialization, prefer value, then fall back to defaultValue
|
|
49
50
|
const initialValue =
|
|
@@ -58,22 +59,21 @@ const InputNumber: React.FC<InputNumberProps> = (props) => {
|
|
|
58
59
|
: "";
|
|
59
60
|
|
|
60
61
|
const [_value, setValue] = useState<string>(initialValue);
|
|
61
|
-
const [lastValue, setLastValue] = useState<string>(initialValue);
|
|
62
62
|
const prevValueRef = useRef<typeof value>(undefined);
|
|
63
|
+
// 最近一次通知出去的文本,代替原来那个用 state 记账的写法
|
|
64
|
+
const notifiedRef = useRef<string>(initialValue);
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
},
|
|
76
|
-
);
|
|
66
|
+
/**
|
|
67
|
+
* `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
|
|
68
|
+
* 所以在事件处理里发,不在 effect 里发。详见 `TextArea/index.tsx` 里那段说明——
|
|
69
|
+
* 旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
|
|
70
|
+
* 会让传内联箭头的消费方死循环。
|
|
71
|
+
*/
|
|
72
|
+
const notify = (next: string) => {
|
|
73
|
+
if (next === notifiedRef.current) return;
|
|
74
|
+
notifiedRef.current = next;
|
|
75
|
+
onChange?.(next);
|
|
76
|
+
};
|
|
77
77
|
|
|
78
78
|
useEffect(() => {
|
|
79
79
|
// Update internal state only when the external value prop actually changes
|
|
@@ -96,17 +96,18 @@ const InputNumber: React.FC<InputNumberProps> = (props) => {
|
|
|
96
96
|
Number(value) === Number(_value);
|
|
97
97
|
|
|
98
98
|
if (sameNumber) {
|
|
99
|
-
//
|
|
100
|
-
|
|
99
|
+
// 只同步「已通知过的值」,不动正在编辑的文本
|
|
100
|
+
notifiedRef.current = _value;
|
|
101
101
|
} else if (value !== undefined && value !== null) {
|
|
102
102
|
const newValue =
|
|
103
103
|
typeof value === "number" ? `${value}` : value?.toString();
|
|
104
|
+
// 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
|
|
105
|
+
notifiedRef.current = newValue;
|
|
104
106
|
setValue(newValue);
|
|
105
|
-
setLastValue(newValue);
|
|
106
107
|
} else {
|
|
107
108
|
// Clear only on initialization or when explicitly set to undefined/null externally
|
|
109
|
+
notifiedRef.current = "";
|
|
108
110
|
setValue("");
|
|
109
|
-
setLastValue("");
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
// `_value` 是刻意不进依赖的:这个 effect 只该在**外部** value 变化时跑,
|
|
@@ -114,15 +115,15 @@ const InputNumber: React.FC<InputNumberProps> = (props) => {
|
|
|
114
115
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
116
|
}, [value]);
|
|
116
117
|
|
|
118
|
+
// 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑
|
|
117
119
|
useEffect(() => {
|
|
118
|
-
|
|
119
|
-
}, [
|
|
120
|
+
getRefRef.current?.(ref.current);
|
|
121
|
+
}, [getRefRef]);
|
|
120
122
|
|
|
121
123
|
const handleClear = (e: React.MouseEvent) => {
|
|
122
124
|
e.stopPropagation();
|
|
123
125
|
setValue("");
|
|
124
|
-
|
|
125
|
-
onChange?.("");
|
|
126
|
+
notify("");
|
|
126
127
|
};
|
|
127
128
|
|
|
128
129
|
const showClear =
|
|
@@ -138,6 +139,7 @@ const InputNumber: React.FC<InputNumberProps> = (props) => {
|
|
|
138
139
|
? ""
|
|
139
140
|
: (typeof e === "number" ? e : e || "").toString();
|
|
140
141
|
setValue(newValue);
|
|
142
|
+
notify(newValue);
|
|
141
143
|
}}
|
|
142
144
|
className={classNames(styles.antdInput, className)}
|
|
143
145
|
controls={false}
|