@arcblock/ux 2.12.20 → 2.12.22
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.
@@ -1,7 +1,7 @@
|
|
1
1
|
import { SelectProps } from '@mui/material';
|
2
2
|
import type { CountryIso2 } from 'react-international-phone';
|
3
3
|
export interface CountryDisplayOptions {
|
4
|
-
|
4
|
+
hideFlag?: boolean;
|
5
5
|
}
|
6
6
|
export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
|
7
7
|
value: CountryIso2;
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
-
import { useMemo, forwardRef, useState
|
2
|
+
import { useMemo, forwardRef, useState } from 'react';
|
3
3
|
import { Box, MenuItem, Select, Typography, TextField } from '@mui/material';
|
4
4
|
import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
|
5
|
+
import ArrowDownwardIcon from '@arcblock/icons/lib/ArrowDown';
|
6
|
+
import { temp as colors } from '../Colors';
|
5
7
|
const CountrySelect = /*#__PURE__*/forwardRef(({
|
6
8
|
value,
|
7
9
|
onChange,
|
@@ -10,14 +12,15 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
10
12
|
preview = false
|
11
13
|
}, ref) => {
|
12
14
|
const {
|
13
|
-
|
15
|
+
hideFlag = true
|
14
16
|
} = selectCountryProps || {};
|
15
17
|
const [searchQuery, setSearchQuery] = useState('');
|
16
18
|
const countryDetail = useMemo(() => {
|
17
19
|
const item = defaultCountries.find(v => v[1] === value);
|
18
20
|
return value && item ? parseCountry(item) : {
|
19
21
|
name: '',
|
20
|
-
iso2: ''
|
22
|
+
iso2: '',
|
23
|
+
dialCode: ''
|
21
24
|
};
|
22
25
|
}, [value]);
|
23
26
|
const filteredCountries = useMemo(() => {
|
@@ -28,10 +31,7 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
28
31
|
return parsed.name.toLowerCase().includes(query) || parsed.iso2.toLowerCase().includes(query) || parsed.dialCode.includes(query);
|
29
32
|
});
|
30
33
|
}, [searchQuery]);
|
31
|
-
const
|
32
|
-
onChange?.(e.target.value);
|
33
|
-
};
|
34
|
-
const renderCountryContent = useCallback(code => /*#__PURE__*/_jsxs(Box, {
|
34
|
+
const renderCountryContent = /*#__PURE__*/_jsxs(Box, {
|
35
35
|
display: "flex",
|
36
36
|
alignItems: "center",
|
37
37
|
flexWrap: "nowrap",
|
@@ -39,26 +39,30 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
39
39
|
sx: {
|
40
40
|
cursor: preview ? 'default' : 'pointer'
|
41
41
|
},
|
42
|
-
children: [/*#__PURE__*/_jsx(FlagEmoji, {
|
43
|
-
iso2:
|
42
|
+
children: [hideFlag ? null : /*#__PURE__*/_jsx(FlagEmoji, {
|
43
|
+
iso2: value,
|
44
44
|
style: {
|
45
45
|
display: 'flex',
|
46
|
-
width:
|
46
|
+
width: 24,
|
47
47
|
color: 'inherit'
|
48
48
|
}
|
49
|
-
}), /*#__PURE__*/
|
50
|
-
|
49
|
+
}), /*#__PURE__*/_jsxs(Typography, {
|
50
|
+
component: "span",
|
51
|
+
sx: {
|
52
|
+
lineHeight: 1.5
|
53
|
+
},
|
54
|
+
children: ["+", countryDetail?.dialCode]
|
51
55
|
})]
|
52
|
-
})
|
56
|
+
});
|
53
57
|
if (preview) {
|
54
|
-
return renderCountryContent
|
58
|
+
return renderCountryContent;
|
55
59
|
}
|
56
60
|
return /*#__PURE__*/_jsxs(Select, {
|
57
61
|
ref: ref,
|
58
62
|
MenuProps: {
|
59
63
|
style: {
|
60
|
-
|
61
|
-
top:
|
64
|
+
maxHeight: 400,
|
65
|
+
top: 2
|
62
66
|
},
|
63
67
|
anchorOrigin: {
|
64
68
|
vertical: 'bottom',
|
@@ -70,21 +74,26 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
70
74
|
}
|
71
75
|
},
|
72
76
|
sx: {
|
73
|
-
width: '100%',
|
74
|
-
fieldset: {
|
75
|
-
display: 'none'
|
76
|
-
},
|
77
77
|
'&.Mui-focused:has(div[aria-expanded="false"])': {
|
78
78
|
fieldset: {
|
79
79
|
display: 'block'
|
80
80
|
}
|
81
81
|
},
|
82
82
|
'.MuiSelect-select': {
|
83
|
-
padding:
|
83
|
+
padding: 1,
|
84
84
|
paddingRight: '24px !important'
|
85
85
|
},
|
86
86
|
svg: {
|
87
|
-
right:
|
87
|
+
right: 4,
|
88
|
+
top: 10
|
89
|
+
},
|
90
|
+
'&:hover': {
|
91
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
92
|
+
borderColor: colors.dividerColor
|
93
|
+
}
|
94
|
+
},
|
95
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
96
|
+
borderColor: colors.dividerColor
|
88
97
|
},
|
89
98
|
'.MuiMenuItem-root': {
|
90
99
|
justifyContent: 'flex-start'
|
@@ -92,8 +101,15 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
92
101
|
...sx
|
93
102
|
},
|
94
103
|
value: value,
|
95
|
-
onChange:
|
96
|
-
|
104
|
+
onChange: e => onChange?.(e.target.value)
|
105
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
106
|
+
,
|
107
|
+
IconComponent: props => /*#__PURE__*/_jsx(ArrowDownwardIcon, {
|
108
|
+
...props,
|
109
|
+
width: 20,
|
110
|
+
height: 20
|
111
|
+
}),
|
112
|
+
renderValue: () => renderCountryContent,
|
97
113
|
children: [/*#__PURE__*/_jsx(Box, {
|
98
114
|
sx: {
|
99
115
|
p: 1,
|
@@ -109,31 +125,24 @@ const CountrySelect = /*#__PURE__*/forwardRef(({
|
|
109
125
|
value: searchQuery,
|
110
126
|
onChange: e => setSearchQuery(e.target.value),
|
111
127
|
onClick: e => e.stopPropagation(),
|
112
|
-
onKeyDown: e => e.stopPropagation()
|
113
|
-
sx: {
|
114
|
-
'& .MuiOutlinedInput-root': {
|
115
|
-
'& fieldset': {
|
116
|
-
borderColor: 'divider'
|
117
|
-
}
|
118
|
-
}
|
119
|
-
}
|
128
|
+
onKeyDown: e => e.stopPropagation()
|
120
129
|
})
|
121
130
|
}), filteredCountries.map(c => {
|
122
131
|
const parsed = parseCountry(c);
|
123
132
|
return /*#__PURE__*/_jsxs(MenuItem, {
|
124
133
|
value: parsed.iso2,
|
125
|
-
children: [/*#__PURE__*/_jsx(FlagEmoji, {
|
134
|
+
children: [hideFlag ? null : /*#__PURE__*/_jsx(FlagEmoji, {
|
126
135
|
iso2: parsed.iso2,
|
127
136
|
style: {
|
128
|
-
marginRight:
|
129
|
-
width:
|
137
|
+
marginRight: 8,
|
138
|
+
width: 24
|
130
139
|
}
|
131
140
|
}), /*#__PURE__*/_jsx(Typography, {
|
132
|
-
marginRight:
|
141
|
+
marginRight: 1,
|
133
142
|
children: parsed.name
|
134
143
|
}), /*#__PURE__*/_jsxs(Typography, {
|
135
144
|
color: "gray",
|
136
|
-
children: ["+", parsed.dialCode]
|
145
|
+
children: ["(+", parsed.dialCode, ")"]
|
137
146
|
})]
|
138
147
|
}, parsed.iso2);
|
139
148
|
})]
|
@@ -12,6 +12,5 @@ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChang
|
|
12
12
|
preview?: boolean;
|
13
13
|
allowDial?: boolean;
|
14
14
|
}
|
15
|
-
export declare function getCountryCallingCode(country: CountryIso2): string;
|
16
15
|
export declare function validatePhoneNumber(phone: string): boolean;
|
17
16
|
export default function PhoneInput({ value, onChange, placeholder, countryDisplayOptions, preview, allowDial, ...props }: PhoneInputProps): import("react/jsx-runtime").JSX.Element;
|
package/lib/PhoneInput/index.js
CHANGED
@@ -1,21 +1,35 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import { useMemo } from 'react';
|
3
|
-
import { Box,
|
3
|
+
import { Box, TextField, Typography } from '@mui/material';
|
4
4
|
import { defaultCountries, usePhoneInput, parseCountry } from 'react-international-phone';
|
5
5
|
import isMobilePhone from 'validator/lib/isMobilePhone';
|
6
6
|
import CountrySelect from './country-select';
|
7
|
-
|
8
|
-
const countryData = defaultCountries.find(c => parseCountry(c).iso2 === country);
|
9
|
-
if (countryData) {
|
10
|
-
return parseCountry(countryData).dialCode.replace('+', '');
|
11
|
-
}
|
12
|
-
return '';
|
13
|
-
}
|
7
|
+
import { mergeSx } from '../Util/style';
|
14
8
|
export function validatePhoneNumber(phone) {
|
15
9
|
return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', {
|
16
10
|
strictMode: true
|
17
11
|
});
|
18
12
|
}
|
13
|
+
|
14
|
+
// 从带区号的电话号码中提取纯号码部分
|
15
|
+
function extractPhoneWithoutCode(phone, dialCode) {
|
16
|
+
if (!phone) return '';
|
17
|
+
// 先去除区号
|
18
|
+
const phoneWithoutCode = phone.replace(new RegExp(`^\\+${dialCode}`), '');
|
19
|
+
// 去除非数字字符,但保留括号
|
20
|
+
return phoneWithoutCode.replace(/[^\d()]/g, '');
|
21
|
+
}
|
22
|
+
|
23
|
+
// 添加区号到纯号码
|
24
|
+
function addCountryCodeToPhone(phone, dialCode) {
|
25
|
+
if (!phone) return '';
|
26
|
+
return phone.startsWith('+') ? phone : `+${dialCode}${phone}`;
|
27
|
+
}
|
28
|
+
|
29
|
+
// 获取不带+号的区号
|
30
|
+
function getDialCodeWithoutPlus(dialCode) {
|
31
|
+
return dialCode.replace(/^\+/, '');
|
32
|
+
}
|
19
33
|
export default function PhoneInput({
|
20
34
|
value = {
|
21
35
|
country: 'us',
|
@@ -28,39 +42,73 @@ export default function PhoneInput({
|
|
28
42
|
allowDial = true,
|
29
43
|
...props
|
30
44
|
}) {
|
31
|
-
//
|
32
|
-
const
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
return
|
39
|
-
}, [
|
45
|
+
// 创建国家代码映射表,避免重复解析
|
46
|
+
const countryMap = useMemo(() => {
|
47
|
+
const map = new Map();
|
48
|
+
defaultCountries.forEach(country => {
|
49
|
+
const parsed = parseCountry(country);
|
50
|
+
map.set(parsed.iso2, parsed);
|
51
|
+
});
|
52
|
+
return map;
|
53
|
+
}, []);
|
54
|
+
|
55
|
+
// 使用 react-international-phone 的 hook
|
40
56
|
const {
|
41
57
|
phone,
|
42
|
-
handlePhoneValueChange,
|
43
58
|
inputRef,
|
44
|
-
country
|
45
|
-
setCountry
|
59
|
+
country
|
46
60
|
} = usePhoneInput({
|
47
61
|
defaultCountry: value.country,
|
48
|
-
value:
|
62
|
+
value: value.phone,
|
49
63
|
countries: defaultCountries,
|
50
64
|
onChange: data => {
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
65
|
+
if (!preview && onChange) {
|
66
|
+
onChange({
|
67
|
+
country: data.country,
|
68
|
+
phone: data.phone
|
69
|
+
});
|
70
|
+
}
|
57
71
|
}
|
58
72
|
});
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
73
|
+
|
74
|
+
// 获取当前国家的区号(不带+号)
|
75
|
+
const currentDialCode = useMemo(() => {
|
76
|
+
const countryData = countryMap.get(country);
|
77
|
+
return countryData ? getDialCodeWithoutPlus(countryData.dialCode) : '';
|
78
|
+
}, [country, countryMap]);
|
79
|
+
|
80
|
+
// 从完整电话号码中提取不带区号的部分用于显示,并去除格式化字符
|
81
|
+
const displayPhone = useMemo(() => extractPhoneWithoutCode(phone, currentDialCode), [phone, currentDialCode]);
|
82
|
+
|
83
|
+
// 创建更新后的 PhoneValue 对象
|
84
|
+
const createUpdatedPhoneValue = (countryIso2, phoneWithoutCode) => {
|
85
|
+
const countryData = countryMap.get(countryIso2);
|
86
|
+
if (!countryData) return {
|
87
|
+
country: countryIso2,
|
88
|
+
phone: ''
|
89
|
+
};
|
90
|
+
const dialCode = getDialCodeWithoutPlus(countryData.dialCode);
|
91
|
+
return {
|
92
|
+
country: countryIso2,
|
93
|
+
phone: addCountryCodeToPhone(phoneWithoutCode, dialCode)
|
94
|
+
};
|
95
|
+
};
|
96
|
+
|
97
|
+
// 处理电话号码变更
|
98
|
+
const onPhoneChange = e => {
|
99
|
+
if (preview) return;
|
100
|
+
// 去除用户输入中的非数字字符,但保留括号
|
101
|
+
const cleanedValue = e.target.value.replace(/[^\d()]/g, '');
|
102
|
+
onChange?.(createUpdatedPhoneValue(country, cleanedValue));
|
103
|
+
};
|
104
|
+
|
105
|
+
// 处理国家变更
|
106
|
+
const onCountryChange = newCountry => {
|
107
|
+
if (preview) return;
|
108
|
+
onChange?.(createUpdatedPhoneValue(newCountry, displayPhone));
|
63
109
|
};
|
110
|
+
|
111
|
+
// 预览模式
|
64
112
|
if (preview) {
|
65
113
|
const isValid = phone && validatePhoneNumber(phone);
|
66
114
|
const canDial = allowDial && isValid;
|
@@ -90,56 +138,41 @@ export default function PhoneInput({
|
|
90
138
|
sx: {
|
91
139
|
ml: 0.5
|
92
140
|
},
|
93
|
-
children:
|
141
|
+
children: displayPhone
|
94
142
|
})]
|
95
143
|
});
|
96
144
|
}
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
145
|
+
// 编辑模式
|
146
|
+
return /*#__PURE__*/_jsxs(Box, {
|
147
|
+
display: "flex",
|
148
|
+
alignItems: "flex-start",
|
149
|
+
gap: 0.5,
|
150
|
+
children: [/*#__PURE__*/_jsx(CountrySelect, {
|
151
|
+
value: country,
|
152
|
+
onChange: onCountryChange,
|
153
|
+
preview: false,
|
154
|
+
selectCountryProps: countryDisplayOptions
|
155
|
+
}), /*#__PURE__*/_jsx(TextField, {
|
156
|
+
...props,
|
157
|
+
value: displayPhone,
|
158
|
+
onChange: onPhoneChange,
|
159
|
+
placeholder: placeholder,
|
160
|
+
className: "phone-input",
|
161
|
+
inputRef: inputRef,
|
162
|
+
sx: mergeSx({
|
163
|
+
'& .MuiInputBase-input': {
|
164
|
+
padding: 1
|
115
165
|
},
|
116
|
-
|
117
|
-
|
166
|
+
'& .MuiFormHelperText-root': {
|
167
|
+
position: 'absolute',
|
168
|
+
bottom: 0,
|
169
|
+
left: 0,
|
170
|
+
transform: 'translateY(100%)',
|
171
|
+
margin: 0
|
118
172
|
}
|
119
|
-
}
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
position: "start",
|
124
|
-
style: {
|
125
|
-
marginRight: '2px',
|
126
|
-
marginLeft: '-8px'
|
127
|
-
},
|
128
|
-
children: /*#__PURE__*/_jsx(CountrySelect, {
|
129
|
-
value: country,
|
130
|
-
onChange: onCountryChange,
|
131
|
-
preview: preview,
|
132
|
-
selectCountryProps: countryDisplayOptions,
|
133
|
-
sx: {
|
134
|
-
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
135
|
-
borderColor: 'transparent'
|
136
|
-
},
|
137
|
-
opacity: preview ? 1 : undefined,
|
138
|
-
cursor: preview ? 'default' : undefined
|
139
|
-
}
|
140
|
-
})
|
141
|
-
}),
|
142
|
-
...(props.InputProps ?? {})
|
143
|
-
}
|
173
|
+
}, props.sx // 这里传入的可能是一个数组或一个对象
|
174
|
+
),
|
175
|
+
InputProps: props.InputProps ?? {}
|
176
|
+
})]
|
144
177
|
});
|
145
178
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.12.
|
3
|
+
"version": "2.12.22",
|
4
4
|
"description": "Common used react components for arcblock products",
|
5
5
|
"keywords": [
|
6
6
|
"react",
|
@@ -68,12 +68,12 @@
|
|
68
68
|
"react": ">=18.2.0",
|
69
69
|
"react-router-dom": ">=6.22.3"
|
70
70
|
},
|
71
|
-
"gitHead": "
|
71
|
+
"gitHead": "f6f7c7949553c8c3e66516f13f768264f480b5d6",
|
72
72
|
"dependencies": {
|
73
73
|
"@arcblock/did-motif": "^1.1.13",
|
74
|
-
"@arcblock/icons": "^2.12.
|
75
|
-
"@arcblock/nft-display": "^2.12.
|
76
|
-
"@arcblock/react-hooks": "^2.12.
|
74
|
+
"@arcblock/icons": "^2.12.22",
|
75
|
+
"@arcblock/nft-display": "^2.12.22",
|
76
|
+
"@arcblock/react-hooks": "^2.12.22",
|
77
77
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
78
78
|
"@fontsource/inter": "^5.0.16",
|
79
79
|
"@fontsource/ubuntu-mono": "^5.0.18",
|
@@ -110,7 +110,7 @@
|
|
110
110
|
"react-error-boundary": "^3.1.4",
|
111
111
|
"react-ga4": "^2.1.0",
|
112
112
|
"react-helmet": "^6.1.0",
|
113
|
-
"react-international-phone": "
|
113
|
+
"react-international-phone": "3.1.2",
|
114
114
|
"react-intersection-observer": "^8.34.0",
|
115
115
|
"react-lottie-player": "^1.4.3",
|
116
116
|
"react-player": "^1.15.3",
|
@@ -1,10 +1,12 @@
|
|
1
|
-
import { useMemo, forwardRef, useState
|
1
|
+
import { useMemo, forwardRef, useState } from 'react';
|
2
2
|
import { Box, MenuItem, Select, Typography, SelectProps, TextField } from '@mui/material';
|
3
3
|
import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
|
4
4
|
import type { CountryIso2 } from 'react-international-phone';
|
5
|
+
import ArrowDownwardIcon from '@arcblock/icons/lib/ArrowDown';
|
6
|
+
import { temp as colors } from '../Colors';
|
5
7
|
|
6
8
|
export interface CountryDisplayOptions {
|
7
|
-
|
9
|
+
hideFlag?: boolean;
|
8
10
|
}
|
9
11
|
|
10
12
|
export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
|
@@ -16,16 +18,17 @@ export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChang
|
|
16
18
|
|
17
19
|
const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
|
18
20
|
({ value, onChange, sx = {}, selectCountryProps, preview = false }, ref) => {
|
19
|
-
const {
|
21
|
+
const { hideFlag = true } = selectCountryProps || {};
|
20
22
|
const [searchQuery, setSearchQuery] = useState('');
|
21
23
|
|
22
24
|
const countryDetail = useMemo(() => {
|
23
25
|
const item = defaultCountries.find((v) => v[1] === value);
|
24
|
-
return value && item ? parseCountry(item) : { name: '', iso2: '' };
|
26
|
+
return value && item ? parseCountry(item) : { name: '', iso2: '', dialCode: '' };
|
25
27
|
}, [value]);
|
26
28
|
|
27
29
|
const filteredCountries = useMemo(() => {
|
28
30
|
if (!searchQuery) return defaultCountries;
|
31
|
+
|
29
32
|
const query = searchQuery.toLowerCase();
|
30
33
|
return defaultCountries.filter((country) => {
|
31
34
|
const parsed = parseCountry(country);
|
@@ -37,71 +40,52 @@ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
|
|
37
40
|
});
|
38
41
|
}, [searchQuery]);
|
39
42
|
|
40
|
-
const
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
<FlagEmoji iso2={code} style={{ display: 'flex', width: '24px', color: 'inherit' }} />
|
53
|
-
<Typography>{showFullName ? countryDetail?.name : countryDetail?.iso2}</Typography>
|
54
|
-
</Box>
|
55
|
-
),
|
56
|
-
[preview, showFullName, countryDetail]
|
43
|
+
const renderCountryContent = (
|
44
|
+
<Box
|
45
|
+
display="flex"
|
46
|
+
alignItems="center"
|
47
|
+
flexWrap="nowrap"
|
48
|
+
gap={0.5}
|
49
|
+
sx={{ cursor: preview ? 'default' : 'pointer' }}>
|
50
|
+
{hideFlag ? null : <FlagEmoji iso2={value} style={{ display: 'flex', width: 24, color: 'inherit' }} />}
|
51
|
+
<Typography component="span" sx={{ lineHeight: 1.5 }}>
|
52
|
+
+{countryDetail?.dialCode}
|
53
|
+
</Typography>
|
54
|
+
</Box>
|
57
55
|
);
|
58
56
|
|
59
57
|
if (preview) {
|
60
|
-
return renderCountryContent
|
58
|
+
return renderCountryContent;
|
61
59
|
}
|
62
60
|
|
63
61
|
return (
|
64
62
|
<Select
|
65
63
|
ref={ref}
|
66
64
|
MenuProps={{
|
67
|
-
style: {
|
68
|
-
|
69
|
-
|
70
|
-
},
|
71
|
-
anchorOrigin: {
|
72
|
-
vertical: 'bottom',
|
73
|
-
horizontal: 'left',
|
74
|
-
},
|
75
|
-
transformOrigin: {
|
76
|
-
vertical: 'top',
|
77
|
-
horizontal: 'left',
|
78
|
-
},
|
65
|
+
style: { maxHeight: 400, top: 2 },
|
66
|
+
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
67
|
+
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
79
68
|
}}
|
80
69
|
sx={{
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
display: 'block',
|
70
|
+
'&.Mui-focused:has(div[aria-expanded="false"])': { fieldset: { display: 'block' } },
|
71
|
+
'.MuiSelect-select': { padding: 1, paddingRight: '24px !important' },
|
72
|
+
svg: { right: 4, top: 10 },
|
73
|
+
'&:hover': {
|
74
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
75
|
+
borderColor: colors.dividerColor,
|
88
76
|
},
|
89
77
|
},
|
90
|
-
'.
|
91
|
-
|
92
|
-
paddingRight: '24px !important',
|
93
|
-
},
|
94
|
-
svg: {
|
95
|
-
right: 0,
|
96
|
-
},
|
97
|
-
'.MuiMenuItem-root': {
|
98
|
-
justifyContent: 'flex-start',
|
78
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
79
|
+
borderColor: colors.dividerColor,
|
99
80
|
},
|
81
|
+
'.MuiMenuItem-root': { justifyContent: 'flex-start' },
|
100
82
|
...sx,
|
101
83
|
}}
|
102
84
|
value={value}
|
103
|
-
onChange={
|
104
|
-
|
85
|
+
onChange={(e) => onChange?.(e.target.value as CountryIso2)}
|
86
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
87
|
+
IconComponent={(props) => <ArrowDownwardIcon {...props} width={20} height={20} />}
|
88
|
+
renderValue={() => renderCountryContent}>
|
105
89
|
<Box sx={{ p: 1, position: 'sticky', top: 0, bgcolor: 'background.paper', zIndex: 1 }}>
|
106
90
|
<TextField
|
107
91
|
size="small"
|
@@ -111,22 +95,16 @@ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
|
|
111
95
|
onChange={(e) => setSearchQuery(e.target.value)}
|
112
96
|
onClick={(e) => e.stopPropagation()}
|
113
97
|
onKeyDown={(e) => e.stopPropagation()}
|
114
|
-
sx={{
|
115
|
-
'& .MuiOutlinedInput-root': {
|
116
|
-
'& fieldset': {
|
117
|
-
borderColor: 'divider',
|
118
|
-
},
|
119
|
-
},
|
120
|
-
}}
|
121
98
|
/>
|
122
99
|
</Box>
|
123
|
-
|
100
|
+
|
101
|
+
{filteredCountries.map((c) => {
|
124
102
|
const parsed = parseCountry(c);
|
125
103
|
return (
|
126
104
|
<MenuItem key={parsed.iso2} value={parsed.iso2}>
|
127
|
-
<FlagEmoji iso2={parsed.iso2} style={{ marginRight:
|
128
|
-
<Typography marginRight=
|
129
|
-
<Typography color="gray"
|
105
|
+
{hideFlag ? null : <FlagEmoji iso2={parsed.iso2} style={{ marginRight: 8, width: 24 }} />}
|
106
|
+
<Typography marginRight={1}>{parsed.name}</Typography>
|
107
|
+
<Typography color="gray">(+{parsed.dialCode})</Typography>
|
130
108
|
</MenuItem>
|
131
109
|
);
|
132
110
|
})}
|
package/src/PhoneInput/index.tsx
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import { useMemo } from 'react';
|
2
|
-
import { Box,
|
2
|
+
import { Box, TextField, TextFieldProps, Typography } from '@mui/material';
|
3
3
|
import { defaultCountries, CountryIso2, usePhoneInput, parseCountry } from 'react-international-phone';
|
4
4
|
import isMobilePhone from 'validator/lib/isMobilePhone';
|
5
5
|
import CountrySelect, { CountryDisplayOptions } from './country-select';
|
6
|
+
import { mergeSx } from '../Util/style';
|
6
7
|
|
7
8
|
export interface PhoneValue {
|
8
9
|
country: CountryIso2;
|
9
|
-
phone: string;
|
10
|
+
phone: string; // 带区号的完整电话号码,如 +86123456789
|
10
11
|
}
|
11
12
|
|
12
13
|
export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
|
@@ -17,18 +18,30 @@ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChang
|
|
17
18
|
allowDial?: boolean; // 是否允许拨号, 只在 preview 为 true 时有效
|
18
19
|
}
|
19
20
|
|
20
|
-
export function getCountryCallingCode(country: CountryIso2): string {
|
21
|
-
const countryData = defaultCountries.find((c) => parseCountry(c).iso2 === country);
|
22
|
-
if (countryData) {
|
23
|
-
return parseCountry(countryData).dialCode.replace('+', '');
|
24
|
-
}
|
25
|
-
return '';
|
26
|
-
}
|
27
|
-
|
28
21
|
export function validatePhoneNumber(phone: string): boolean {
|
29
22
|
return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', { strictMode: true });
|
30
23
|
}
|
31
24
|
|
25
|
+
// 从带区号的电话号码中提取纯号码部分
|
26
|
+
function extractPhoneWithoutCode(phone: string, dialCode: string): string {
|
27
|
+
if (!phone) return '';
|
28
|
+
// 先去除区号
|
29
|
+
const phoneWithoutCode = phone.replace(new RegExp(`^\\+${dialCode}`), '');
|
30
|
+
// 去除非数字字符,但保留括号
|
31
|
+
return phoneWithoutCode.replace(/[^\d()]/g, '');
|
32
|
+
}
|
33
|
+
|
34
|
+
// 添加区号到纯号码
|
35
|
+
function addCountryCodeToPhone(phone: string, dialCode: string): string {
|
36
|
+
if (!phone) return '';
|
37
|
+
return phone.startsWith('+') ? phone : `+${dialCode}${phone}`;
|
38
|
+
}
|
39
|
+
|
40
|
+
// 获取不带+号的区号
|
41
|
+
function getDialCodeWithoutPlus(dialCode: string): string {
|
42
|
+
return dialCode.replace(/^\+/, '');
|
43
|
+
}
|
44
|
+
|
32
45
|
export default function PhoneInput({
|
33
46
|
value = { country: 'us', phone: '' },
|
34
47
|
onChange,
|
@@ -38,36 +51,67 @@ export default function PhoneInput({
|
|
38
51
|
allowDial = true,
|
39
52
|
...props
|
40
53
|
}: PhoneInputProps) {
|
41
|
-
//
|
42
|
-
const
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
return
|
49
|
-
}, [
|
50
|
-
|
51
|
-
|
54
|
+
// 创建国家代码映射表,避免重复解析
|
55
|
+
const countryMap = useMemo(() => {
|
56
|
+
const map = new Map<CountryIso2, ReturnType<typeof parseCountry>>();
|
57
|
+
defaultCountries.forEach((country) => {
|
58
|
+
const parsed = parseCountry(country);
|
59
|
+
map.set(parsed.iso2, parsed);
|
60
|
+
});
|
61
|
+
return map;
|
62
|
+
}, []);
|
63
|
+
|
64
|
+
// 使用 react-international-phone 的 hook
|
65
|
+
const { phone, inputRef, country } = usePhoneInput({
|
52
66
|
defaultCountry: value.country,
|
53
|
-
value:
|
67
|
+
value: value.phone,
|
54
68
|
countries: defaultCountries,
|
55
69
|
onChange: (data) => {
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
70
|
+
if (!preview && onChange) {
|
71
|
+
onChange({
|
72
|
+
country: data.country,
|
73
|
+
phone: data.phone,
|
74
|
+
});
|
75
|
+
}
|
62
76
|
},
|
63
77
|
});
|
64
78
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
79
|
+
// 获取当前国家的区号(不带+号)
|
80
|
+
const currentDialCode = useMemo(() => {
|
81
|
+
const countryData = countryMap.get(country);
|
82
|
+
return countryData ? getDialCodeWithoutPlus(countryData.dialCode) : '';
|
83
|
+
}, [country, countryMap]);
|
84
|
+
|
85
|
+
// 从完整电话号码中提取不带区号的部分用于显示,并去除格式化字符
|
86
|
+
const displayPhone = useMemo(() => extractPhoneWithoutCode(phone, currentDialCode), [phone, currentDialCode]);
|
87
|
+
|
88
|
+
// 创建更新后的 PhoneValue 对象
|
89
|
+
const createUpdatedPhoneValue = (countryIso2: CountryIso2, phoneWithoutCode: string): PhoneValue => {
|
90
|
+
const countryData = countryMap.get(countryIso2);
|
91
|
+
if (!countryData) return { country: countryIso2, phone: '' };
|
92
|
+
|
93
|
+
const dialCode = getDialCodeWithoutPlus(countryData.dialCode);
|
94
|
+
return {
|
95
|
+
country: countryIso2,
|
96
|
+
phone: addCountryCodeToPhone(phoneWithoutCode, dialCode),
|
97
|
+
};
|
98
|
+
};
|
99
|
+
|
100
|
+
// 处理电话号码变更
|
101
|
+
const onPhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
102
|
+
if (preview) return;
|
103
|
+
// 去除用户输入中的非数字字符,但保留括号
|
104
|
+
const cleanedValue = e.target.value.replace(/[^\d()]/g, '');
|
105
|
+
onChange?.(createUpdatedPhoneValue(country, cleanedValue));
|
69
106
|
};
|
70
107
|
|
108
|
+
// 处理国家变更
|
109
|
+
const onCountryChange = (newCountry: CountryIso2) => {
|
110
|
+
if (preview) return;
|
111
|
+
onChange?.(createUpdatedPhoneValue(newCountry, displayPhone));
|
112
|
+
};
|
113
|
+
|
114
|
+
// 预览模式
|
71
115
|
if (preview) {
|
72
116
|
const isValid = phone && validatePhoneNumber(phone);
|
73
117
|
const canDial = allowDial && isValid;
|
@@ -95,57 +139,43 @@ export default function PhoneInput({
|
|
95
139
|
},
|
96
140
|
}}>
|
97
141
|
<CountrySelect value={country} preview={preview} />
|
98
|
-
<Typography sx={{ ml: 0.5 }}>{
|
142
|
+
<Typography sx={{ ml: 0.5 }}>{displayPhone}</Typography>
|
99
143
|
</Box>
|
100
144
|
);
|
101
145
|
}
|
146
|
+
// 编辑模式
|
102
147
|
return (
|
103
|
-
<
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
preview={preview}
|
136
|
-
selectCountryProps={countryDisplayOptions}
|
137
|
-
sx={{
|
138
|
-
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
139
|
-
borderColor: 'transparent',
|
140
|
-
},
|
141
|
-
opacity: preview ? 1 : undefined,
|
142
|
-
cursor: preview ? 'default' : undefined,
|
143
|
-
}}
|
144
|
-
/>
|
145
|
-
</InputAdornment>
|
146
|
-
),
|
147
|
-
...(props.InputProps ?? {}),
|
148
|
-
}}
|
149
|
-
/>
|
148
|
+
<Box display="flex" alignItems="flex-start" gap={0.5}>
|
149
|
+
<CountrySelect
|
150
|
+
value={country}
|
151
|
+
onChange={onCountryChange}
|
152
|
+
preview={false}
|
153
|
+
selectCountryProps={countryDisplayOptions}
|
154
|
+
/>
|
155
|
+
<TextField
|
156
|
+
{...props}
|
157
|
+
value={displayPhone}
|
158
|
+
onChange={onPhoneChange}
|
159
|
+
placeholder={placeholder}
|
160
|
+
className="phone-input"
|
161
|
+
inputRef={inputRef}
|
162
|
+
sx={mergeSx(
|
163
|
+
{
|
164
|
+
'& .MuiInputBase-input': {
|
165
|
+
padding: 1,
|
166
|
+
},
|
167
|
+
'& .MuiFormHelperText-root': {
|
168
|
+
position: 'absolute',
|
169
|
+
bottom: 0,
|
170
|
+
left: 0,
|
171
|
+
transform: 'translateY(100%)',
|
172
|
+
margin: 0,
|
173
|
+
},
|
174
|
+
},
|
175
|
+
props.sx as any // 这里传入的可能是一个数组或一个对象
|
176
|
+
)}
|
177
|
+
InputProps={props.InputProps ?? {}}
|
178
|
+
/>
|
179
|
+
</Box>
|
150
180
|
);
|
151
181
|
}
|