@tendaui/components 1.0.0 → 1.2.3

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 (270) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +176 -176
  3. package/alert/Alert.tsx +3 -2
  4. package/button/_example/base.tsx +10 -0
  5. package/button/_example/icon.tsx +20 -0
  6. package/color-picker/ColorPickPanel.tsx +9 -0
  7. package/color-picker/ColorPicker.tsx +67 -0
  8. package/color-picker/components/panel/alpha.tsx +32 -0
  9. package/color-picker/components/panel/format/index.tsx +47 -0
  10. package/color-picker/components/panel/format/inputs.tsx +119 -0
  11. package/color-picker/components/panel/header.tsx +37 -0
  12. package/color-picker/components/panel/hue.tsx +20 -0
  13. package/color-picker/components/panel/index.tsx +191 -0
  14. package/color-picker/components/panel/saturation.tsx +81 -0
  15. package/color-picker/components/panel/slider.tsx +76 -0
  16. package/color-picker/components/panel/swatches.tsx +84 -0
  17. package/color-picker/components/trigger.tsx +49 -0
  18. package/color-picker/defaultProps.ts +7 -0
  19. package/color-picker/helpers.ts +53 -0
  20. package/color-picker/hooks/useClassNames.ts +9 -0
  21. package/color-picker/hooks/useStyles.ts +39 -0
  22. package/color-picker/index.ts +12 -0
  23. package/color-picker/style/css.js +1 -0
  24. package/color-picker/style/index.js +1 -0
  25. package/color-picker/type.ts +143 -0
  26. package/color-picker/utils/color-picker/cmyk.ts +89 -0
  27. package/color-picker/utils/color-picker/color.ts +467 -0
  28. package/color-picker/utils/color-picker/constants.ts +187 -0
  29. package/color-picker/utils/color-picker/draggable.ts +100 -0
  30. package/color-picker/utils/color-picker/format.ts +95 -0
  31. package/color-picker/utils/color-picker/gradient.ts +243 -0
  32. package/color-picker/utils/color-picker/index.ts +7 -0
  33. package/color-picker/utils/color-picker/types.ts +33 -0
  34. package/common/observe.ts +33 -0
  35. package/common.ts +20 -0
  36. package/config-provider/ConfigContext.tsx +4 -1
  37. package/config-provider/index.ts +1 -1
  38. package/dialog/DialogCard.tsx +4 -6
  39. package/dialog/hooks/useDialogPosition.ts +1 -2
  40. package/dialog/plugin.tsx +3 -2
  41. package/drawer/Drawer.tsx +264 -0
  42. package/drawer/defaultProps.ts +19 -0
  43. package/drawer/hooks/useDrag.ts +98 -0
  44. package/drawer/hooks/useLockStyle.ts +36 -0
  45. package/drawer/index.ts +5 -0
  46. package/drawer/style/css.js +1 -0
  47. package/drawer/style/index.js +1 -0
  48. package/drawer/type.ts +193 -0
  49. package/drawer/utils/index.ts +76 -0
  50. package/fireworks/Fireworks.tsx +138 -0
  51. package/fireworks/index.ts +10 -0
  52. package/fireworks/style/css.js +0 -0
  53. package/fireworks/style/index.js +0 -0
  54. package/fireworks/type.ts +72 -0
  55. package/form/FormItem.tsx +5 -5
  56. package/form/easing.ts +10 -0
  57. package/form/scroll.ts +124 -0
  58. package/form/type.ts +519 -519
  59. package/global-config/default-config.ts +95 -0
  60. package/global-config/locale/ar_KW.ts +270 -0
  61. package/global-config/locale/en_US.ts +280 -0
  62. package/global-config/locale/it_IT.ts +287 -0
  63. package/global-config/locale/ja_JP.ts +279 -0
  64. package/global-config/locale/ko_KR.ts +279 -0
  65. package/global-config/locale/ru_RU.ts +288 -0
  66. package/global-config/locale/zh_CN.ts +279 -0
  67. package/global-config/locale/zh_TW.ts +279 -0
  68. package/global-config/mobile/default-config.ts +6 -0
  69. package/global-config/mobile/locale/ar_KW.ts +113 -0
  70. package/global-config/mobile/locale/en_US.ts +114 -0
  71. package/global-config/mobile/locale/it_IT.ts +114 -0
  72. package/global-config/mobile/locale/ja_JP.ts +101 -0
  73. package/global-config/mobile/locale/ko_KR.ts +101 -0
  74. package/global-config/mobile/locale/ru_RU.ts +113 -0
  75. package/global-config/mobile/locale/zh_CN.ts +101 -0
  76. package/global-config/mobile/locale/zh_TW.ts +101 -0
  77. package/global-config/t.ts +111 -0
  78. package/hooks/useControlled.ts +3 -3
  79. package/hooks/useDeepEffect.ts +32 -0
  80. package/hooks/useGlobalIcon.ts +10 -3
  81. package/hooks/useLastest.ts +2 -6
  82. package/hooks/useResizeObserve.ts +36 -0
  83. package/index.ts +10 -7
  84. package/input/Input.tsx +4 -1
  85. package/input/defaultProps.ts +0 -2
  86. package/input/type.ts +1 -6
  87. package/input-number/InputNumber.tsx +124 -0
  88. package/input-number/defaultProps.ts +17 -0
  89. package/input-number/index.ts +9 -0
  90. package/input-number/style/css.js +1 -0
  91. package/input-number/style/index.js +1 -0
  92. package/input-number/type.ts +147 -0
  93. package/input-number/useInputNumber.tsx +270 -0
  94. package/ip-input/IPInput.tsx +516 -0
  95. package/ip-input/defaultProps.ts +11 -0
  96. package/ip-input/index.ts +3 -0
  97. package/ip-input/style/css.js +1 -0
  98. package/ip-input/style/index.js +1 -0
  99. package/ip-input/type.ts +115 -0
  100. package/ip-input/utils.ts +112 -0
  101. package/layout/Aside.tsx +38 -0
  102. package/layout/Layout.tsx +104 -0
  103. package/layout/defaultProps.ts +9 -0
  104. package/layout/index.ts +9 -0
  105. package/layout/style/css.js +1 -0
  106. package/layout/style/index.js +1 -0
  107. package/layout/type.ts +43 -0
  108. package/list/List.tsx +144 -0
  109. package/list/ListItem.tsx +36 -0
  110. package/list/ListItemMeta.tsx +40 -0
  111. package/list/defaultProps.ts +11 -0
  112. package/list/hooks/useListVirtualScroll.ts +82 -0
  113. package/list/index.ts +11 -0
  114. package/list/style/css.js +1 -0
  115. package/list/style/index.js +1 -0
  116. package/list/type.ts +93 -0
  117. package/locale/LocalReceiver.ts +55 -0
  118. package/locale/ar_KW.ts +7 -0
  119. package/locale/en_US.ts +7 -0
  120. package/locale/it_IT.ts +6 -0
  121. package/locale/ja_JP.ts +6 -0
  122. package/locale/ko_KR.ts +6 -0
  123. package/locale/ru_RU.ts +6 -0
  124. package/locale/zh_CN.ts +5 -0
  125. package/locale/zh_TW.ts +7 -0
  126. package/notification/NotifyContainer.tsx +2 -2
  127. package/notification/NotifyContext.tsx +1 -0
  128. package/package.json +6 -3
  129. package/popup/Popup.tsx +34 -10
  130. package/radio/Radio.tsx +24 -0
  131. package/radio/RadioGroup.tsx +159 -0
  132. package/radio/defaultProps.ts +18 -0
  133. package/radio/index.ts +12 -0
  134. package/radio/style/css.js +0 -0
  135. package/radio/style/index.js +1 -0
  136. package/radio/type.ts +115 -0
  137. package/radio/useKeyboard.ts +36 -0
  138. package/select/hooks/useOptions.ts +10 -7
  139. package/select/hooks/usePanelVirtualScroll.ts +1 -1
  140. package/select/type.ts +382 -382
  141. package/select-input/type.ts +280 -280
  142. package/slider/Slider.tsx +270 -0
  143. package/slider/SliderHandleButton.tsx +50 -0
  144. package/slider/defaultProps.ts +15 -0
  145. package/slider/index.ts +9 -0
  146. package/slider/style/css.js +1 -0
  147. package/slider/style/index.js +1 -0
  148. package/slider/type.ts +77 -0
  149. package/style/all.js +26 -0
  150. package/styles/_global.scss +39 -39
  151. package/styles/_vars.scss +358 -386
  152. package/styles/components/alert/_index.scss +175 -175
  153. package/styles/components/alert/_vars.scss +39 -39
  154. package/styles/components/badge/_index.scss +70 -70
  155. package/styles/components/badge/_vars.scss +25 -25
  156. package/styles/components/button/_index.scss +499 -511
  157. package/styles/components/button/_mixins.scss +39 -39
  158. package/styles/components/button/_vars.scss +120 -122
  159. package/styles/components/checkbox/_index.scss +158 -158
  160. package/styles/components/checkbox/_var.scss +60 -60
  161. package/styles/components/color-picker/_index.scss +586 -0
  162. package/styles/components/color-picker/_mixins.scss +0 -0
  163. package/styles/components/color-picker/_vars.scss +84 -0
  164. package/styles/components/dialog/_animate.scss +135 -135
  165. package/styles/components/dialog/_index.scss +311 -311
  166. package/styles/components/dialog/_vars.scss +59 -59
  167. package/styles/components/drawer/_index.scss +205 -0
  168. package/styles/components/drawer/_mixins.scss +1 -0
  169. package/styles/components/drawer/_var.scss +53 -0
  170. package/styles/components/fireworks/_index.scss +86 -0
  171. package/styles/components/fireworks/_vars.scss +4 -0
  172. package/styles/components/form/_index.scss +174 -174
  173. package/styles/components/form/_mixins.scss +76 -76
  174. package/styles/components/form/_vars.scss +100 -100
  175. package/styles/components/input/_index.scss +349 -349
  176. package/styles/components/input/_mixins.scss +116 -116
  177. package/styles/components/input/_vars.scss +134 -134
  178. package/styles/components/input-number/_index.scss +353 -0
  179. package/styles/components/input-number/_mixins.scss +0 -0
  180. package/styles/components/input-number/_vars.scss +65 -0
  181. package/styles/components/ip-input/_index.scss +280 -0
  182. package/styles/components/layout/_index.scss +47 -0
  183. package/styles/components/layout/_mixin.scss +0 -0
  184. package/styles/components/layout/_vars.scss +18 -0
  185. package/styles/components/layout/doc.scss +74 -0
  186. package/styles/components/list/_index.scss +172 -0
  187. package/styles/components/list/_mixins.scss +0 -0
  188. package/styles/components/list/_vars.scss +41 -0
  189. package/styles/components/loading/_index.scss +112 -112
  190. package/styles/components/loading/_vars.scss +39 -39
  191. package/styles/components/notification/_index.scss +160 -160
  192. package/styles/components/notification/_mixins.scss +12 -12
  193. package/styles/components/notification/_vars.scss +59 -59
  194. package/styles/components/popup/_index.scss +82 -82
  195. package/styles/components/popup/_mixin.scss +149 -149
  196. package/styles/components/popup/_var.scss +31 -31
  197. package/styles/components/radio/_index.scss +376 -0
  198. package/styles/components/radio/_mixins.scss +0 -0
  199. package/styles/components/radio/_var.scss +92 -0
  200. package/styles/components/select/_index.scss +290 -290
  201. package/styles/components/select/_var.scss +65 -65
  202. package/styles/components/select-input/_index.scss +5 -5
  203. package/styles/components/select-input/_var.scss +3 -3
  204. package/styles/components/slider/_index.scss +241 -0
  205. package/styles/components/slider/_mixins.scss +0 -0
  206. package/styles/components/slider/_vars.scss +50 -0
  207. package/styles/components/switch/_index.scss +279 -279
  208. package/styles/components/switch/_vars.scss +61 -61
  209. package/styles/components/table/_index.scss +193 -0
  210. package/styles/components/table/_var.scss +52 -0
  211. package/styles/components/tabs/_index.scss +165 -0
  212. package/styles/components/tabs/_mixins.scss +11 -0
  213. package/styles/components/tabs/_vars.scss +71 -0
  214. package/styles/components/tag/_index.scss +316 -316
  215. package/styles/components/tag/_var.scss +85 -85
  216. package/styles/components/tag-input/_index.scss +163 -163
  217. package/styles/components/tag-input/_vars.scss +16 -16
  218. package/styles/globals.css +250 -250
  219. package/styles/mixins/_focus.scss +7 -7
  220. package/styles/mixins/_layout.scss +32 -32
  221. package/styles/mixins/_reset.scss +10 -10
  222. package/styles/mixins/_scrollbar.scss +31 -31
  223. package/styles/mixins/_text.scss +48 -48
  224. package/styles/rillple.css +16 -16
  225. package/styles/scrollbar.css +41 -41
  226. package/styles/themes/_dark.scss +191 -191
  227. package/styles/themes/_font.scss +69 -79
  228. package/styles/themes/_index.scss +5 -5
  229. package/styles/themes/_light.scss +190 -190
  230. package/styles/themes/_radius.scss +9 -9
  231. package/styles/themes/_size.scss +68 -68
  232. package/styles/themes.css +66 -66
  233. package/styles/utilities/_animation.scss +57 -57
  234. package/styles/utilities/_tips.scss +9 -9
  235. package/tab/TabBar.tsx +85 -0
  236. package/tab/TabNav.tsx +103 -0
  237. package/tab/TabNavItem.tsx +80 -0
  238. package/tab/TabPanel.tsx +42 -0
  239. package/tab/Tabs.tsx +71 -0
  240. package/tab/defaultProps.ts +19 -0
  241. package/tab/index.ts +7 -0
  242. package/tab/style/index.js +1 -0
  243. package/tab/type.ts +125 -0
  244. package/tab/useTabClass.ts +20 -0
  245. package/table/Cell.tsx +109 -0
  246. package/table/TBody.tsx +77 -0
  247. package/table/THead.tsx +63 -0
  248. package/table/TR.tsx +78 -0
  249. package/table/Table.tsx +73 -0
  250. package/table/defaultProps.ts +14 -0
  251. package/table/hooks/index.ts +4 -0
  252. package/table/hooks/useTableClassName.ts +63 -0
  253. package/table/hooks/useTableStyle.ts +93 -0
  254. package/table/index.ts +7 -0
  255. package/table/style/css.js +1 -0
  256. package/table/style/index.js +1 -0
  257. package/table/type.ts +192 -0
  258. package/tag/Tag.tsx +1 -1
  259. package/tag-input/hooks/useTagList.tsx +1 -1
  260. package/utils/dom.ts +4 -0
  261. package/utils/forwardRefWithStatics.ts +1 -4
  262. package/utils/input-number/large-number.ts +423 -0
  263. package/utils/input-number/number.ts +257 -0
  264. package/utils/isFragment.ts +6 -6
  265. package/utils/log/index.ts +3 -0
  266. package/utils/log/log.ts +30 -0
  267. package/utils/log/types.ts +12 -0
  268. package/utils/number.ts +21 -0
  269. package/utils/scroll.ts +26 -0
  270. package/utils/style.ts +2 -4
@@ -0,0 +1,516 @@
1
+ import React, {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ forwardRef,
7
+ useCallback,
8
+ KeyboardEvent,
9
+ ClipboardEvent,
10
+ ChangeEvent
11
+ } from "react";
12
+ import classNames from "classnames";
13
+ import { IconClose } from "@tendaui/icons";
14
+ import { IPInputProps, IPInputRef } from "./type";
15
+ import useDefaultProps from "../hooks/useDefaultProps";
16
+ import useControlled from "../hooks/useControlled";
17
+ import useConfig from "../hooks/useConfig";
18
+ import { ipInputDefaultProps } from "./defaultProps";
19
+ import { extractIPFromText, parseIPv4ToSegments, segmentsToIPv4 } from "./utils";
20
+ import "./style/index.js";
21
+
22
+ const IPInput = forwardRef<IPInputRef, IPInputProps>((originalProps, ref) => {
23
+ const props = useDefaultProps<IPInputProps>(originalProps, ipInputDefaultProps as Partial<IPInputProps>);
24
+ const { classPrefix } = useConfig();
25
+
26
+ const {
27
+ onChange: onChangeProp,
28
+ onBlur: onBlurProp,
29
+ onFocus: onFocusProp,
30
+ allowIPv6 = false,
31
+ allowCIDR = false,
32
+ placeholder,
33
+ disabled,
34
+ readOnly = false,
35
+ showClear = false,
36
+ autoFocus = false,
37
+ showSegmentSeparators = true,
38
+ formatter,
39
+ id,
40
+ name,
41
+ ariaLabel,
42
+ segmentClassName,
43
+ inputStyle,
44
+ separator,
45
+ tips,
46
+ className,
47
+ style
48
+ } = props;
49
+
50
+ // 受控/非受控值管理
51
+ const [value, onChange] = useControlled(props, "value", onChangeProp);
52
+
53
+ // IPv4 模式:四个输入框的状态
54
+ const [segments, setSegments] = useState<string[]>(() => {
55
+ if (value) {
56
+ const parsed = parseIPv4ToSegments(value.split("/")[0]);
57
+ return parsed;
58
+ }
59
+ return ["", "", "", ""];
60
+ });
61
+
62
+ // CIDR 掩码状态
63
+ const [cidrMask, setCidrMask] = useState<string>(() => {
64
+ if (value && value.includes("/")) {
65
+ return value.split("/")[1] || "";
66
+ }
67
+ return "";
68
+ });
69
+
70
+ // IPv6 模式:单输入框状态
71
+ const [ipv6Value, setIpv6Value] = useState<string>(() => {
72
+ if (value && allowIPv6 && value.includes(":")) {
73
+ return value.split("/")[0];
74
+ }
75
+ return "";
76
+ });
77
+
78
+ // 输入框引用
79
+ const segmentRefs = useRef<(HTMLInputElement | null)[]>([]);
80
+ const cidrRef = useRef<HTMLInputElement | null>(null);
81
+ const ipv6Ref = useRef<HTMLInputElement | null>(null);
82
+
83
+ // UI 状态
84
+ const [isFocused, setIsFocused] = useState(false);
85
+ const [isHover, setIsHover] = useState(false);
86
+ const [focusedSegmentIndex, setFocusedSegmentIndex] = useState<number>(-1);
87
+
88
+ // 判断当前是否为 IPv6 模式
89
+ const isIPv6Mode = allowIPv6 && (ipv6Value || (value && value.includes(":")));
90
+
91
+ // 同步外部 value 到内部状态
92
+ useEffect(() => {
93
+ if (value !== undefined) {
94
+ if (isIPv6Mode) {
95
+ const ip = value.split("/")[0];
96
+ setIpv6Value(ip);
97
+ if (value.includes("/")) {
98
+ setCidrMask(value.split("/")[1] || "");
99
+ } else {
100
+ setCidrMask("");
101
+ }
102
+ } else {
103
+ const parsed = parseIPv4ToSegments(value.split("/")[0]);
104
+ setSegments(parsed);
105
+ if (value.includes("/")) {
106
+ setCidrMask(value.split("/")[1] || "");
107
+ } else {
108
+ setCidrMask("");
109
+ }
110
+ }
111
+ }
112
+ }, [value, isIPv6Mode]);
113
+
114
+ // 获取当前完整值
115
+ const getCurrentValue = useCallback((): string => {
116
+ if (isIPv6Mode) {
117
+ if (cidrMask) {
118
+ return `${ipv6Value}/${cidrMask}`;
119
+ }
120
+ return ipv6Value;
121
+ } else {
122
+ const ip = segmentsToIPv4(segments);
123
+
124
+ if (cidrMask) {
125
+ return `${ip}/${cidrMask}`;
126
+ }
127
+ return ip;
128
+ }
129
+ }, [isIPv6Mode, ipv6Value, segments, cidrMask]);
130
+
131
+ // 触发 onChange
132
+ const triggerChange = useCallback(
133
+ (newValue: string) => {
134
+ // 应用格式化函数(如果有)
135
+ const formattedValue = formatter ? formatter(newValue) : newValue;
136
+ onChange?.(formattedValue);
137
+ },
138
+ [formatter, onChange]
139
+ );
140
+
141
+ // IPv4 段输入处理
142
+ const handleSegmentChange = useCallback(
143
+ (index: number, newValue: string) => {
144
+ // 只允许数字
145
+ const numericValue = newValue.replace(/[^\d]/g, "");
146
+
147
+ // 限制长度
148
+ let finalValue = numericValue;
149
+ if (numericValue.length > 3) {
150
+ finalValue = numericValue.slice(0, 3);
151
+ }
152
+
153
+ // 更新段
154
+ const newSegments = [...segments];
155
+ newSegments[index] = finalValue;
156
+ setSegments(newSegments);
157
+
158
+ // 自动跳转到下一段
159
+ if (finalValue.length === 3 && index < 3) {
160
+ segmentRefs.current[index + 1]?.focus();
161
+ }
162
+
163
+ // 组合完整 IP
164
+ const ip = segmentsToIPv4(newSegments);
165
+ const fullValue = cidrMask ? `${ip}/${cidrMask}` : ip;
166
+ triggerChange(fullValue);
167
+ },
168
+ [segments, cidrMask, triggerChange]
169
+ );
170
+
171
+ // IPv4 段键盘事件
172
+ const handleSegmentKeyDown = useCallback((index: number, e: KeyboardEvent<HTMLInputElement>) => {
173
+ const input = e.currentTarget;
174
+ const value = input.value;
175
+ const cursorPos = input.selectionStart || 0;
176
+
177
+ // 右箭头:移动到下一段或末尾
178
+ if (e.key === "ArrowRight") {
179
+ if (cursorPos === value.length && index < 3) {
180
+ e.preventDefault();
181
+ segmentRefs.current[index + 1]?.focus();
182
+ }
183
+ }
184
+
185
+ // 左箭头:移动到上一段或开头
186
+ if (e.key === "ArrowLeft") {
187
+ if (cursorPos === 0 && index > 0) {
188
+ e.preventDefault();
189
+ segmentRefs.current[index - 1]?.focus();
190
+ }
191
+ }
192
+
193
+ // Backspace:在开头时跳转到上一段
194
+ if (e.key === "Backspace") {
195
+ if (cursorPos === 0 && value === "" && index > 0) {
196
+ e.preventDefault();
197
+ const prevInput = segmentRefs.current[index - 1];
198
+ if (prevInput) {
199
+ prevInput.focus();
200
+ prevInput.setSelectionRange(prevInput.value.length, prevInput.value.length);
201
+ }
202
+ }
203
+ }
204
+
205
+ // 点号或空格:跳转到下一段
206
+ if ((e.key === "." || e.key === " ") && index < 3) {
207
+ e.preventDefault();
208
+ segmentRefs.current[index + 1]?.focus();
209
+ }
210
+
211
+ // Delete:删除当前字符
212
+ if (e.key === "Delete" && cursorPos === value.length && index < 3) {
213
+ // 可以在这里添加特殊处理
214
+ }
215
+ }, []);
216
+
217
+ // IPv4 段粘贴处理
218
+ const handleSegmentPaste = useCallback(
219
+ (e: ClipboardEvent<HTMLInputElement>) => {
220
+ e.preventDefault();
221
+ const pasteText = e.clipboardData.getData("text/plain");
222
+ const extracted = extractIPFromText(pasteText);
223
+
224
+ if (extracted) {
225
+ if (extracted.ip.includes(":")) {
226
+ // IPv6
227
+ if (allowIPv6) {
228
+ setIpv6Value(extracted.ip);
229
+ if (extracted.cidr) {
230
+ setCidrMask(extracted.cidr);
231
+ }
232
+ const fullValue = extracted.cidr ? `${extracted.ip}/${extracted.cidr}` : extracted.ip;
233
+ triggerChange(fullValue);
234
+ }
235
+ } else {
236
+ // IPv4
237
+ const parsed = parseIPv4ToSegments(extracted.ip);
238
+ setSegments(parsed);
239
+ if (extracted.cidr && allowCIDR) {
240
+ setCidrMask(extracted.cidr);
241
+ }
242
+ const fullValue = extracted.cidr && allowCIDR ? `${extracted.ip}/${extracted.cidr}` : extracted.ip;
243
+ triggerChange(fullValue);
244
+ }
245
+ }
246
+ },
247
+ [allowIPv6, allowCIDR, triggerChange]
248
+ );
249
+
250
+ // CIDR 掩码输入处理
251
+ const handleCIDRChange = useCallback(
252
+ (e: ChangeEvent<HTMLInputElement>) => {
253
+ const newMask = e.target.value.replace(/[^\d]/g, "");
254
+ const maxLength = isIPv6Mode ? 3 : 2;
255
+ const finalMask = newMask.slice(0, maxLength);
256
+
257
+ setCidrMask(finalMask);
258
+
259
+ const ip = isIPv6Mode ? ipv6Value : segmentsToIPv4(segments);
260
+ const fullValue = finalMask ? `${ip}/${finalMask}` : ip;
261
+ triggerChange(fullValue);
262
+ },
263
+ [isIPv6Mode, ipv6Value, segments, triggerChange]
264
+ );
265
+
266
+ // IPv6 输入处理
267
+ const handleIPv6Change = useCallback(
268
+ (e: ChangeEvent<HTMLInputElement>) => {
269
+ const newValue = e.target.value;
270
+ setIpv6Value(newValue);
271
+
272
+ const fullValue = cidrMask ? `${newValue}/${cidrMask}` : newValue;
273
+ triggerChange(fullValue);
274
+ },
275
+ [cidrMask, triggerChange]
276
+ );
277
+
278
+ // IPv6 粘贴处理
279
+ const handleIPv6Paste = useCallback(
280
+ (e: ClipboardEvent<HTMLInputElement>) => {
281
+ e.preventDefault();
282
+ const pasteText = e.clipboardData.getData("text/plain");
283
+ const extracted = extractIPFromText(pasteText);
284
+
285
+ if (extracted) {
286
+ setIpv6Value(extracted.ip);
287
+ if (extracted.cidr && allowCIDR) {
288
+ setCidrMask(extracted.cidr);
289
+ }
290
+ const fullValue = extracted.cidr && allowCIDR ? `${extracted.ip}/${extracted.cidr}` : extracted.ip;
291
+ triggerChange(fullValue);
292
+ }
293
+ },
294
+ [allowCIDR, triggerChange]
295
+ );
296
+
297
+ // 焦点处理
298
+ const handleFocus = useCallback(
299
+ (index?: number) => {
300
+ setIsFocused(true);
301
+ if (index !== undefined) {
302
+ setFocusedSegmentIndex(index);
303
+ }
304
+ onFocusProp?.();
305
+ },
306
+ [onFocusProp]
307
+ );
308
+
309
+ // 失焦处理
310
+ const handleBlur = useCallback(() => {
311
+ setIsFocused(false);
312
+ setFocusedSegmentIndex(-1);
313
+
314
+ const currentValue = getCurrentValue();
315
+ onBlurProp?.(currentValue);
316
+ }, [getCurrentValue, onBlurProp]);
317
+
318
+ // 清空处理
319
+ const handleClear = useCallback(
320
+ (e: React.MouseEvent) => {
321
+ e.stopPropagation();
322
+ if (disabled || readOnly) return;
323
+
324
+ setSegments(["", "", "", ""]);
325
+ setIpv6Value("");
326
+ setCidrMask("");
327
+ onChange?.("");
328
+ },
329
+ [disabled, readOnly, onChange]
330
+ );
331
+
332
+ // 暴露给父组件的方法
333
+ useImperativeHandle(ref, () => ({
334
+ focus: () => {
335
+ if (isIPv6Mode) {
336
+ ipv6Ref.current?.focus();
337
+ } else {
338
+ segmentRefs.current[0]?.focus();
339
+ }
340
+ },
341
+ blur: () => {
342
+ if (isIPv6Mode) {
343
+ ipv6Ref.current?.blur();
344
+ } else {
345
+ segmentRefs.current.forEach((ref) => ref?.blur());
346
+ }
347
+ cidrRef.current?.blur();
348
+ },
349
+ clear: () => {
350
+ setSegments(["", "", "", ""]);
351
+ setIpv6Value("");
352
+ setCidrMask("");
353
+ onChange?.("");
354
+ },
355
+ getValue: () => getCurrentValue()
356
+ }));
357
+
358
+ // 自动聚焦
359
+ useEffect(() => {
360
+ if (autoFocus && !disabled && !readOnly) {
361
+ if (isIPv6Mode) {
362
+ ipv6Ref.current?.focus();
363
+ } else {
364
+ segmentRefs.current[0]?.focus();
365
+ }
366
+ }
367
+ }, [autoFocus, disabled, readOnly, isIPv6Mode]);
368
+
369
+ const showClearButton = showClear && isHover && getCurrentValue() && !disabled && !readOnly;
370
+
371
+ // 分隔符
372
+ const displaySeparator = separator || (isIPv6Mode ? ":" : ".");
373
+
374
+ // 渲染 IPv4 模式
375
+ const renderIPv4Input = () => {
376
+ return (
377
+ <div className={`${classPrefix}-ip-input__segments`}>
378
+ {segments.map((segment, index) => {
379
+ return (
380
+ <React.Fragment key={index}>
381
+ <input
382
+ ref={(el) => {
383
+ segmentRefs.current[index] = el;
384
+ }}
385
+ type="text"
386
+ inputMode="numeric"
387
+ pattern="[0-9]*"
388
+ maxLength={3}
389
+ value={segment}
390
+ onChange={(e) => handleSegmentChange(index, e.target.value)}
391
+ onKeyDown={(e) => handleSegmentKeyDown(index, e)}
392
+ onPaste={handleSegmentPaste}
393
+ onFocus={() => handleFocus(index)}
394
+ onBlur={handleBlur}
395
+ disabled={disabled}
396
+ readOnly={readOnly}
397
+ placeholder={placeholder ? placeholder.split(".")[index] : undefined}
398
+ className={classNames(`${classPrefix}-ip-input__segment`, segmentClassName, {
399
+ [`${classPrefix}-ip-input__segment--focused`]: focusedSegmentIndex === index
400
+ })}
401
+ style={inputStyle}
402
+ id={index === 0 ? id : undefined}
403
+ name={index === 0 ? name : undefined}
404
+ aria-label={ariaLabel ? `${ariaLabel} 第 ${index + 1} 段` : `IP 地址第 ${index + 1} 段`}
405
+ />
406
+ {index < 3 && showSegmentSeparators && (
407
+ <span className={`${classPrefix}-ip-input__separator`}>{displaySeparator}</span>
408
+ )}
409
+ </React.Fragment>
410
+ );
411
+ })}
412
+ {allowCIDR && (
413
+ <>
414
+ <span className={`${classPrefix}-ip-input__separator`}>/</span>
415
+ <input
416
+ ref={cidrRef}
417
+ type="text"
418
+ inputMode="numeric"
419
+ pattern="[0-9]*"
420
+ maxLength={2}
421
+ value={cidrMask}
422
+ onChange={handleCIDRChange}
423
+ onFocus={() => handleFocus()}
424
+ onBlur={handleBlur}
425
+ disabled={disabled}
426
+ readOnly={readOnly}
427
+ placeholder="24"
428
+ className={classNames(`${classPrefix}-ip-input__cidr`, segmentClassName, {
429
+ [`${classPrefix}-ip-input__cidr--error`]: cidrMask && !/^(0|[1-9]\d?|3[0-2])$/.test(cidrMask)
430
+ })}
431
+ style={inputStyle}
432
+ aria-label={ariaLabel ? `${ariaLabel} 掩码` : "CIDR 掩码"}
433
+ />
434
+ </>
435
+ )}
436
+ </div>
437
+ );
438
+ };
439
+
440
+ // 渲染 IPv6 模式
441
+ const renderIPv6Input = () => {
442
+ return (
443
+ <div className={`${classPrefix}-ip-input__ipv6-wrapper`}>
444
+ <input
445
+ ref={ipv6Ref}
446
+ type="text"
447
+ value={ipv6Value}
448
+ onChange={handleIPv6Change}
449
+ onPaste={handleIPv6Paste}
450
+ onFocus={() => handleFocus()}
451
+ onBlur={handleBlur}
452
+ disabled={disabled}
453
+ readOnly={readOnly}
454
+ placeholder={placeholder || "2001:db8::1"}
455
+ className={classNames(`${classPrefix}-ip-input__ipv6`, segmentClassName)}
456
+ style={inputStyle}
457
+ id={id}
458
+ name={name}
459
+ aria-label={ariaLabel || "IPv6 地址"}
460
+ />
461
+ {allowCIDR && (
462
+ <>
463
+ <span className={`${classPrefix}-ip-input__separator`}>/</span>
464
+ <input
465
+ ref={cidrRef}
466
+ type="text"
467
+ inputMode="numeric"
468
+ pattern="[0-9]*"
469
+ maxLength={3}
470
+ value={cidrMask}
471
+ onChange={handleCIDRChange}
472
+ onFocus={() => handleFocus()}
473
+ onBlur={handleBlur}
474
+ disabled={disabled}
475
+ readOnly={readOnly}
476
+ placeholder="64"
477
+ className={classNames(`${classPrefix}-ip-input__cidr`, segmentClassName)}
478
+ style={inputStyle}
479
+ aria-label={ariaLabel ? `${ariaLabel} 掩码` : "CIDR 掩码"}
480
+ />
481
+ </>
482
+ )}
483
+ </div>
484
+ );
485
+ };
486
+
487
+ return (
488
+ <div
489
+ className={classNames(`${classPrefix}-ip-input`, className, {
490
+ [`${classPrefix}-is-disabled`]: disabled,
491
+ [`${classPrefix}-is-readonly`]: readOnly,
492
+ [`${classPrefix}-is-focused`]: isFocused
493
+ // 不在这里添加 is-error,因为每个段有自己的错误状态
494
+ })}
495
+ style={style}
496
+ onMouseEnter={() => setIsHover(true)}
497
+ onMouseLeave={() => setIsHover(false)}
498
+ >
499
+ <div className={`${classPrefix}-ip-input__wrapper`}>
500
+ {isIPv6Mode ? renderIPv6Input() : renderIPv4Input()}
501
+ {showClearButton && (
502
+ <IconClose
503
+ className={`${classPrefix}-ip-input__clear ${classPrefix}-icon`}
504
+ onClick={handleClear}
505
+ onMouseDown={(e) => e.stopPropagation()}
506
+ />
507
+ )}
508
+ </div>
509
+ {tips && <div className={`${classPrefix}-ip-input__tips`}>{tips}</div>}
510
+ </div>
511
+ );
512
+ });
513
+
514
+ IPInput.displayName = "IPInput";
515
+
516
+ export default IPInput;
@@ -0,0 +1,11 @@
1
+ import { TdIPInputProps } from "./type";
2
+
3
+ export const ipInputDefaultProps: Partial<TdIPInputProps> = {
4
+ allowIPv6: false,
5
+ allowCIDR: false,
6
+ readOnly: false,
7
+ showClear: false,
8
+ autoFocus: false,
9
+ showSegmentSeparators: true,
10
+ defaultValue: ""
11
+ };
@@ -0,0 +1,3 @@
1
+ export { default as IPInput } from "./IPInput";
2
+ export type { IPInputProps, IPInputRef, TdIPInputProps } from "./type";
3
+ export * from "./utils";
@@ -0,0 +1 @@
1
+ import "./index.css";
@@ -0,0 +1 @@
1
+ import "../../styles/components/ip-input/_index.scss";
@@ -0,0 +1,115 @@
1
+ import { StyledProps } from "../common";
2
+
3
+ export interface TdIPInputProps {
4
+ /**
5
+ * 受控值,格式为 "192.168.0.1" 或 "2001:db8::1" 或 "192.168.0.1/24"
6
+ */
7
+ value?: string;
8
+ /**
9
+ * 非受控初始值
10
+ */
11
+ defaultValue?: string;
12
+ /**
13
+ * 值变化时触发
14
+ */
15
+ onChange?: (value: string) => void;
16
+ /**
17
+ * 失去焦点时触发
18
+ */
19
+ onBlur?: (value: string) => void;
20
+ /**
21
+ * 获得焦点时触发
22
+ */
23
+ onFocus?: () => void;
24
+ /**
25
+ * 是否允许 IPv6
26
+ * @default false
27
+ */
28
+ allowIPv6?: boolean;
29
+ /**
30
+ * 是否允许 CIDR 格式(带掩码)
31
+ * @default false
32
+ */
33
+ allowCIDR?: boolean;
34
+ /**
35
+ * 占位符
36
+ */
37
+ placeholder?: string;
38
+ /**
39
+ * 是否禁用
40
+ */
41
+ disabled?: boolean;
42
+ /**
43
+ * 是否只读
44
+ * @default false
45
+ */
46
+ readOnly?: boolean;
47
+ /**
48
+ * 是否显示清空按钮
49
+ * @default false
50
+ */
51
+ showClear?: boolean;
52
+ /**
53
+ * 是否自动聚焦第一个输入框
54
+ * @default false
55
+ */
56
+ autoFocus?: boolean;
57
+ /**
58
+ * 是否显示分隔符(仅视觉,不可编辑)
59
+ * @default true
60
+ */
61
+ showSegmentSeparators?: boolean;
62
+ /**
63
+ * 自定义格式化函数
64
+ */
65
+ formatter?: (value: string) => string;
66
+ /**
67
+ * 输入框 ID
68
+ */
69
+ id?: string;
70
+ /**
71
+ * 输入框 name 属性
72
+ */
73
+ name?: string;
74
+ /**
75
+ * aria-label
76
+ */
77
+ ariaLabel?: string;
78
+ /**
79
+ * 段输入框的 className
80
+ */
81
+ segmentClassName?: string;
82
+ /**
83
+ * 段输入框的 style
84
+ */
85
+ inputStyle?: React.CSSProperties;
86
+ /**
87
+ * 分隔符(默认根据 IPv4/IPv6 自动选择)
88
+ */
89
+ separator?: string;
90
+ /**
91
+ * 提示文本
92
+ */
93
+ tips?: string;
94
+ }
95
+
96
+ export interface IPInputProps extends TdIPInputProps, StyledProps {}
97
+
98
+ export interface IPInputRef {
99
+ /**
100
+ * 聚焦第一个输入框
101
+ */
102
+ focus: () => void;
103
+ /**
104
+ * 失焦
105
+ */
106
+ blur: () => void;
107
+ /**
108
+ * 清空所有输入
109
+ */
110
+ clear: () => void;
111
+ /**
112
+ * 获取当前值
113
+ */
114
+ getValue: () => string;
115
+ }