@arcblock/ux 2.12.15 → 2.12.17
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.
@@ -0,0 +1,13 @@
|
|
1
|
+
import { SelectProps } from '@mui/material';
|
2
|
+
import type { CountryIso2 } from 'react-international-phone';
|
3
|
+
export interface CountryDisplayOptions {
|
4
|
+
showFullName?: boolean;
|
5
|
+
}
|
6
|
+
export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
|
7
|
+
value: CountryIso2;
|
8
|
+
onChange?: (value: CountryIso2) => void;
|
9
|
+
selectCountryProps?: CountryDisplayOptions;
|
10
|
+
preview?: boolean;
|
11
|
+
}
|
12
|
+
declare const CountrySelect: import("react").ForwardRefExoticComponent<Omit<CountrySelectProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
13
|
+
export default CountrySelect;
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
+
import { useMemo, forwardRef, useState, useCallback } from 'react';
|
3
|
+
import { Box, MenuItem, Select, Typography, TextField } from '@mui/material';
|
4
|
+
import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
|
5
|
+
const CountrySelect = /*#__PURE__*/forwardRef(({
|
6
|
+
value,
|
7
|
+
onChange,
|
8
|
+
sx = {},
|
9
|
+
selectCountryProps,
|
10
|
+
preview = false
|
11
|
+
}, ref) => {
|
12
|
+
const {
|
13
|
+
showFullName = false
|
14
|
+
} = selectCountryProps || {};
|
15
|
+
const [searchQuery, setSearchQuery] = useState('');
|
16
|
+
const countryDetail = useMemo(() => {
|
17
|
+
const item = defaultCountries.find(v => v[1] === value);
|
18
|
+
return value && item ? parseCountry(item) : {
|
19
|
+
name: '',
|
20
|
+
iso2: ''
|
21
|
+
};
|
22
|
+
}, [value]);
|
23
|
+
const filteredCountries = useMemo(() => {
|
24
|
+
if (!searchQuery) return defaultCountries;
|
25
|
+
const query = searchQuery.toLowerCase();
|
26
|
+
return defaultCountries.filter(country => {
|
27
|
+
const parsed = parseCountry(country);
|
28
|
+
return parsed.name.toLowerCase().includes(query) || parsed.iso2.toLowerCase().includes(query) || parsed.dialCode.includes(query);
|
29
|
+
});
|
30
|
+
}, [searchQuery]);
|
31
|
+
const onCountryChange = e => {
|
32
|
+
onChange?.(e.target.value);
|
33
|
+
};
|
34
|
+
const renderCountryContent = useCallback(code => /*#__PURE__*/_jsxs(Box, {
|
35
|
+
display: "flex",
|
36
|
+
alignItems: "center",
|
37
|
+
flexWrap: "nowrap",
|
38
|
+
gap: 0.5,
|
39
|
+
sx: {
|
40
|
+
cursor: preview ? 'default' : 'pointer'
|
41
|
+
},
|
42
|
+
children: [/*#__PURE__*/_jsx(FlagEmoji, {
|
43
|
+
iso2: code,
|
44
|
+
style: {
|
45
|
+
display: 'flex',
|
46
|
+
width: '24px',
|
47
|
+
color: 'inherit'
|
48
|
+
}
|
49
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
50
|
+
children: showFullName ? countryDetail?.name : countryDetail?.iso2
|
51
|
+
})]
|
52
|
+
}), [preview, showFullName, countryDetail]);
|
53
|
+
if (preview) {
|
54
|
+
return renderCountryContent(value);
|
55
|
+
}
|
56
|
+
return /*#__PURE__*/_jsxs(Select, {
|
57
|
+
ref: ref,
|
58
|
+
MenuProps: {
|
59
|
+
style: {
|
60
|
+
height: '300px',
|
61
|
+
top: '10px'
|
62
|
+
},
|
63
|
+
anchorOrigin: {
|
64
|
+
vertical: 'bottom',
|
65
|
+
horizontal: 'left'
|
66
|
+
},
|
67
|
+
transformOrigin: {
|
68
|
+
vertical: 'top',
|
69
|
+
horizontal: 'left'
|
70
|
+
}
|
71
|
+
},
|
72
|
+
sx: {
|
73
|
+
width: '100%',
|
74
|
+
fieldset: {
|
75
|
+
display: 'none'
|
76
|
+
},
|
77
|
+
'&.Mui-focused:has(div[aria-expanded="false"])': {
|
78
|
+
fieldset: {
|
79
|
+
display: 'block'
|
80
|
+
}
|
81
|
+
},
|
82
|
+
'.MuiSelect-select': {
|
83
|
+
padding: '8px',
|
84
|
+
paddingRight: '24px !important'
|
85
|
+
},
|
86
|
+
svg: {
|
87
|
+
right: 0
|
88
|
+
},
|
89
|
+
'.MuiMenuItem-root': {
|
90
|
+
justifyContent: 'flex-start'
|
91
|
+
},
|
92
|
+
...sx
|
93
|
+
},
|
94
|
+
value: value,
|
95
|
+
onChange: onCountryChange,
|
96
|
+
renderValue: renderCountryContent,
|
97
|
+
children: [/*#__PURE__*/_jsx(Box, {
|
98
|
+
sx: {
|
99
|
+
p: 1,
|
100
|
+
position: 'sticky',
|
101
|
+
top: 0,
|
102
|
+
bgcolor: 'background.paper',
|
103
|
+
zIndex: 1
|
104
|
+
},
|
105
|
+
children: /*#__PURE__*/_jsx(TextField, {
|
106
|
+
size: "small",
|
107
|
+
fullWidth: true,
|
108
|
+
placeholder: "Search country...",
|
109
|
+
value: searchQuery,
|
110
|
+
onChange: e => setSearchQuery(e.target.value),
|
111
|
+
onClick: e => e.stopPropagation(),
|
112
|
+
onKeyDown: e => e.stopPropagation(),
|
113
|
+
sx: {
|
114
|
+
'& .MuiOutlinedInput-root': {
|
115
|
+
'& fieldset': {
|
116
|
+
borderColor: 'divider'
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
})
|
121
|
+
}), filteredCountries.map(c => {
|
122
|
+
const parsed = parseCountry(c);
|
123
|
+
return /*#__PURE__*/_jsxs(MenuItem, {
|
124
|
+
value: parsed.iso2,
|
125
|
+
children: [/*#__PURE__*/_jsx(FlagEmoji, {
|
126
|
+
iso2: parsed.iso2,
|
127
|
+
style: {
|
128
|
+
marginRight: '8px',
|
129
|
+
width: '24px'
|
130
|
+
}
|
131
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
132
|
+
marginRight: "8px",
|
133
|
+
children: parsed.name
|
134
|
+
}), /*#__PURE__*/_jsxs(Typography, {
|
135
|
+
color: "gray",
|
136
|
+
children: ["+", parsed.dialCode]
|
137
|
+
})]
|
138
|
+
}, parsed.iso2);
|
139
|
+
})]
|
140
|
+
});
|
141
|
+
});
|
142
|
+
export default CountrySelect;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { TextFieldProps } from '@mui/material';
|
2
|
+
import { CountryIso2 } from 'react-international-phone';
|
3
|
+
import { CountryDisplayOptions } from './country-select';
|
4
|
+
export interface PhoneValue {
|
5
|
+
country: CountryIso2;
|
6
|
+
phone: string;
|
7
|
+
}
|
8
|
+
export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
|
9
|
+
value?: PhoneValue;
|
10
|
+
onChange?: (value: PhoneValue) => void;
|
11
|
+
countryDisplayOptions?: CountryDisplayOptions;
|
12
|
+
preview?: boolean;
|
13
|
+
allowDial?: boolean;
|
14
|
+
}
|
15
|
+
export declare function getCountryCallingCode(country: CountryIso2): string;
|
16
|
+
export declare function validatePhoneNumber(phone: string): boolean;
|
17
|
+
export default function PhoneInput({ value, onChange, placeholder, countryDisplayOptions, preview, allowDial, ...props }: PhoneInputProps): import("react/jsx-runtime").JSX.Element;
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
+
import { useMemo } from 'react';
|
3
|
+
import { Box, InputAdornment, TextField, Typography } from '@mui/material';
|
4
|
+
import { defaultCountries, usePhoneInput, parseCountry } from 'react-international-phone';
|
5
|
+
import isMobilePhone from 'validator/lib/isMobilePhone';
|
6
|
+
import CountrySelect from './country-select';
|
7
|
+
export function getCountryCallingCode(country) {
|
8
|
+
const countryData = defaultCountries.find(c => parseCountry(c).iso2 === country);
|
9
|
+
if (countryData) {
|
10
|
+
return parseCountry(countryData).dialCode.replace('+', '');
|
11
|
+
}
|
12
|
+
return '';
|
13
|
+
}
|
14
|
+
export function validatePhoneNumber(phone) {
|
15
|
+
return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', {
|
16
|
+
strictMode: true
|
17
|
+
});
|
18
|
+
}
|
19
|
+
export default function PhoneInput({
|
20
|
+
value = {
|
21
|
+
country: 'us',
|
22
|
+
phone: ''
|
23
|
+
},
|
24
|
+
onChange,
|
25
|
+
placeholder = 'Enter phone number',
|
26
|
+
countryDisplayOptions = {},
|
27
|
+
preview = false,
|
28
|
+
allowDial = true,
|
29
|
+
...props
|
30
|
+
}) {
|
31
|
+
// 在使用 usePhoneInput 之前处理电话号码
|
32
|
+
const phoneWithPrefix = useMemo(() => {
|
33
|
+
if (!value.phone) return '';
|
34
|
+
// 如果电话号码已经包含 + 号,说明已经有区号了
|
35
|
+
if (value.phone.startsWith('+')) return value.phone;
|
36
|
+
// 获取国家区号并添加到号码前
|
37
|
+
const countryCode = getCountryCallingCode(value.country);
|
38
|
+
return `+${countryCode}${value.phone}`;
|
39
|
+
}, [value.phone, value.country]);
|
40
|
+
const {
|
41
|
+
phone,
|
42
|
+
handlePhoneValueChange,
|
43
|
+
country,
|
44
|
+
setCountry
|
45
|
+
} = usePhoneInput({
|
46
|
+
defaultCountry: value.country,
|
47
|
+
value: phoneWithPrefix,
|
48
|
+
countries: defaultCountries,
|
49
|
+
onChange: data => {
|
50
|
+
// 确保类型匹配 PhoneValue 接口
|
51
|
+
const phoneValue = {
|
52
|
+
country: data.country,
|
53
|
+
phone: data.phone
|
54
|
+
};
|
55
|
+
onChange?.(phoneValue);
|
56
|
+
}
|
57
|
+
});
|
58
|
+
const onCountryChange = v => {
|
59
|
+
if (!preview) {
|
60
|
+
setCountry(v);
|
61
|
+
}
|
62
|
+
};
|
63
|
+
if (preview) {
|
64
|
+
const isValid = phone && validatePhoneNumber(phone);
|
65
|
+
const canDial = allowDial && isValid;
|
66
|
+
return /*#__PURE__*/_jsxs(Box, {
|
67
|
+
display: "flex",
|
68
|
+
alignItems: "center",
|
69
|
+
component: canDial ? 'a' : 'div',
|
70
|
+
href: canDial ? `tel:${phone}` : undefined,
|
71
|
+
sx: {
|
72
|
+
textDecoration: 'none',
|
73
|
+
color: 'inherit',
|
74
|
+
cursor: canDial ? 'pointer' : 'default',
|
75
|
+
'&:hover': {
|
76
|
+
opacity: canDial ? 0.8 : 1
|
77
|
+
}
|
78
|
+
},
|
79
|
+
children: [/*#__PURE__*/_jsx(CountrySelect, {
|
80
|
+
value: country,
|
81
|
+
preview: preview
|
82
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
83
|
+
sx: {
|
84
|
+
ml: 0.5
|
85
|
+
},
|
86
|
+
children: phone
|
87
|
+
})]
|
88
|
+
});
|
89
|
+
}
|
90
|
+
return /*#__PURE__*/_jsx(TextField, {
|
91
|
+
...props,
|
92
|
+
value: phone,
|
93
|
+
onChange: preview ? undefined : handlePhoneValueChange,
|
94
|
+
placeholder: placeholder,
|
95
|
+
className: "phone-input",
|
96
|
+
disabled: preview,
|
97
|
+
sx: {
|
98
|
+
'&>.MuiInputBase-root': {
|
99
|
+
paddingLeft: '8px',
|
100
|
+
cursor: preview ? 'default' : undefined
|
101
|
+
},
|
102
|
+
...(props.sx ?? {}),
|
103
|
+
...(preview ? {
|
104
|
+
'& .Mui-disabled': {
|
105
|
+
'-webkit-text-fill-color': 'inherit',
|
106
|
+
color: 'inherit'
|
107
|
+
},
|
108
|
+
fieldset: {
|
109
|
+
display: 'none'
|
110
|
+
}
|
111
|
+
} : {})
|
112
|
+
},
|
113
|
+
InputProps: {
|
114
|
+
startAdornment: /*#__PURE__*/_jsx(InputAdornment, {
|
115
|
+
position: "start",
|
116
|
+
style: {
|
117
|
+
marginRight: '2px',
|
118
|
+
marginLeft: '-8px'
|
119
|
+
},
|
120
|
+
children: /*#__PURE__*/_jsx(CountrySelect, {
|
121
|
+
value: country,
|
122
|
+
onChange: onCountryChange,
|
123
|
+
preview: preview,
|
124
|
+
selectCountryProps: countryDisplayOptions,
|
125
|
+
sx: {
|
126
|
+
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
127
|
+
borderColor: 'transparent'
|
128
|
+
},
|
129
|
+
opacity: preview ? 1 : undefined,
|
130
|
+
cursor: preview ? 'default' : undefined
|
131
|
+
}
|
132
|
+
})
|
133
|
+
}),
|
134
|
+
...(props.InputProps ?? {})
|
135
|
+
}
|
136
|
+
});
|
137
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.12.
|
3
|
+
"version": "2.12.17",
|
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": "de1814f4c2eca44000845ce0db42e9d30d487e80",
|
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.17",
|
75
|
+
"@arcblock/nft-display": "^2.12.17",
|
76
|
+
"@arcblock/react-hooks": "^2.12.17",
|
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,6 +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": "^3.1.2",
|
113
114
|
"react-intersection-observer": "^8.34.0",
|
114
115
|
"react-lottie-player": "^1.4.3",
|
115
116
|
"react-player": "^1.15.3",
|
@@ -119,6 +120,7 @@
|
|
119
120
|
"rebound": "^0.1.0",
|
120
121
|
"topojson-client": "^3.1.0",
|
121
122
|
"type-fest": "^4.28.0",
|
123
|
+
"validator": "^13.9.0",
|
122
124
|
"versor": "^0.0.4"
|
123
125
|
}
|
124
126
|
}
|
@@ -0,0 +1,138 @@
|
|
1
|
+
import { useMemo, forwardRef, useState, useCallback } from 'react';
|
2
|
+
import { Box, MenuItem, Select, Typography, SelectProps, TextField } from '@mui/material';
|
3
|
+
import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
|
4
|
+
import type { CountryIso2 } from 'react-international-phone';
|
5
|
+
|
6
|
+
export interface CountryDisplayOptions {
|
7
|
+
showFullName?: boolean;
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
|
11
|
+
value: CountryIso2;
|
12
|
+
onChange?: (value: CountryIso2) => void;
|
13
|
+
selectCountryProps?: CountryDisplayOptions;
|
14
|
+
preview?: boolean;
|
15
|
+
}
|
16
|
+
|
17
|
+
const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
|
18
|
+
({ value, onChange, sx = {}, selectCountryProps, preview = false }, ref) => {
|
19
|
+
const { showFullName = false } = selectCountryProps || {};
|
20
|
+
const [searchQuery, setSearchQuery] = useState('');
|
21
|
+
|
22
|
+
const countryDetail = useMemo(() => {
|
23
|
+
const item = defaultCountries.find((v) => v[1] === value);
|
24
|
+
return value && item ? parseCountry(item) : { name: '', iso2: '' };
|
25
|
+
}, [value]);
|
26
|
+
|
27
|
+
const filteredCountries = useMemo(() => {
|
28
|
+
if (!searchQuery) return defaultCountries;
|
29
|
+
const query = searchQuery.toLowerCase();
|
30
|
+
return defaultCountries.filter((country) => {
|
31
|
+
const parsed = parseCountry(country);
|
32
|
+
return (
|
33
|
+
parsed.name.toLowerCase().includes(query) ||
|
34
|
+
parsed.iso2.toLowerCase().includes(query) ||
|
35
|
+
parsed.dialCode.includes(query)
|
36
|
+
);
|
37
|
+
});
|
38
|
+
}, [searchQuery]);
|
39
|
+
|
40
|
+
const onCountryChange = (e: any) => {
|
41
|
+
onChange?.(e.target.value as CountryIso2);
|
42
|
+
};
|
43
|
+
|
44
|
+
const renderCountryContent = useCallback(
|
45
|
+
(code: CountryIso2) => (
|
46
|
+
<Box
|
47
|
+
display="flex"
|
48
|
+
alignItems="center"
|
49
|
+
flexWrap="nowrap"
|
50
|
+
gap={0.5}
|
51
|
+
sx={{ cursor: preview ? 'default' : 'pointer' }}>
|
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]
|
57
|
+
);
|
58
|
+
|
59
|
+
if (preview) {
|
60
|
+
return renderCountryContent(value);
|
61
|
+
}
|
62
|
+
|
63
|
+
return (
|
64
|
+
<Select
|
65
|
+
ref={ref}
|
66
|
+
MenuProps={{
|
67
|
+
style: {
|
68
|
+
height: '300px',
|
69
|
+
top: '10px',
|
70
|
+
},
|
71
|
+
anchorOrigin: {
|
72
|
+
vertical: 'bottom',
|
73
|
+
horizontal: 'left',
|
74
|
+
},
|
75
|
+
transformOrigin: {
|
76
|
+
vertical: 'top',
|
77
|
+
horizontal: 'left',
|
78
|
+
},
|
79
|
+
}}
|
80
|
+
sx={{
|
81
|
+
width: '100%',
|
82
|
+
fieldset: {
|
83
|
+
display: 'none',
|
84
|
+
},
|
85
|
+
'&.Mui-focused:has(div[aria-expanded="false"])': {
|
86
|
+
fieldset: {
|
87
|
+
display: 'block',
|
88
|
+
},
|
89
|
+
},
|
90
|
+
'.MuiSelect-select': {
|
91
|
+
padding: '8px',
|
92
|
+
paddingRight: '24px !important',
|
93
|
+
},
|
94
|
+
svg: {
|
95
|
+
right: 0,
|
96
|
+
},
|
97
|
+
'.MuiMenuItem-root': {
|
98
|
+
justifyContent: 'flex-start',
|
99
|
+
},
|
100
|
+
...sx,
|
101
|
+
}}
|
102
|
+
value={value}
|
103
|
+
onChange={onCountryChange}
|
104
|
+
renderValue={renderCountryContent}>
|
105
|
+
<Box sx={{ p: 1, position: 'sticky', top: 0, bgcolor: 'background.paper', zIndex: 1 }}>
|
106
|
+
<TextField
|
107
|
+
size="small"
|
108
|
+
fullWidth
|
109
|
+
placeholder="Search country..."
|
110
|
+
value={searchQuery}
|
111
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
112
|
+
onClick={(e) => e.stopPropagation()}
|
113
|
+
onKeyDown={(e) => e.stopPropagation()}
|
114
|
+
sx={{
|
115
|
+
'& .MuiOutlinedInput-root': {
|
116
|
+
'& fieldset': {
|
117
|
+
borderColor: 'divider',
|
118
|
+
},
|
119
|
+
},
|
120
|
+
}}
|
121
|
+
/>
|
122
|
+
</Box>
|
123
|
+
{filteredCountries.map((c: any) => {
|
124
|
+
const parsed = parseCountry(c);
|
125
|
+
return (
|
126
|
+
<MenuItem key={parsed.iso2} value={parsed.iso2}>
|
127
|
+
<FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px', width: '24px' }} />
|
128
|
+
<Typography marginRight="8px">{parsed.name}</Typography>
|
129
|
+
<Typography color="gray">+{parsed.dialCode}</Typography>
|
130
|
+
</MenuItem>
|
131
|
+
);
|
132
|
+
})}
|
133
|
+
</Select>
|
134
|
+
);
|
135
|
+
}
|
136
|
+
);
|
137
|
+
|
138
|
+
export default CountrySelect;
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import { useMemo } from 'react';
|
2
|
+
import { Box, InputAdornment, TextField, TextFieldProps, Typography } from '@mui/material';
|
3
|
+
import { defaultCountries, CountryIso2, usePhoneInput, parseCountry } from 'react-international-phone';
|
4
|
+
import isMobilePhone from 'validator/lib/isMobilePhone';
|
5
|
+
import CountrySelect, { CountryDisplayOptions } from './country-select';
|
6
|
+
|
7
|
+
export interface PhoneValue {
|
8
|
+
country: CountryIso2;
|
9
|
+
phone: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
|
13
|
+
value?: PhoneValue;
|
14
|
+
onChange?: (value: PhoneValue) => void;
|
15
|
+
countryDisplayOptions?: CountryDisplayOptions;
|
16
|
+
preview?: boolean;
|
17
|
+
allowDial?: boolean; // 是否允许拨号, 只在 preview 为 true 时有效
|
18
|
+
}
|
19
|
+
|
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
|
+
export function validatePhoneNumber(phone: string): boolean {
|
29
|
+
return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', { strictMode: true });
|
30
|
+
}
|
31
|
+
|
32
|
+
export default function PhoneInput({
|
33
|
+
value = { country: 'us', phone: '' },
|
34
|
+
onChange,
|
35
|
+
placeholder = 'Enter phone number',
|
36
|
+
countryDisplayOptions = {},
|
37
|
+
preview = false,
|
38
|
+
allowDial = true,
|
39
|
+
...props
|
40
|
+
}: PhoneInputProps) {
|
41
|
+
// 在使用 usePhoneInput 之前处理电话号码
|
42
|
+
const phoneWithPrefix = useMemo(() => {
|
43
|
+
if (!value.phone) return '';
|
44
|
+
// 如果电话号码已经包含 + 号,说明已经有区号了
|
45
|
+
if (value.phone.startsWith('+')) return value.phone;
|
46
|
+
// 获取国家区号并添加到号码前
|
47
|
+
const countryCode = getCountryCallingCode(value.country);
|
48
|
+
return `+${countryCode}${value.phone}`;
|
49
|
+
}, [value.phone, value.country]);
|
50
|
+
|
51
|
+
const { phone, handlePhoneValueChange, country, setCountry } = usePhoneInput({
|
52
|
+
defaultCountry: value.country,
|
53
|
+
value: phoneWithPrefix,
|
54
|
+
countries: defaultCountries,
|
55
|
+
onChange: (data) => {
|
56
|
+
// 确保类型匹配 PhoneValue 接口
|
57
|
+
const phoneValue: PhoneValue = {
|
58
|
+
country: data.country,
|
59
|
+
phone: data.phone,
|
60
|
+
};
|
61
|
+
onChange?.(phoneValue);
|
62
|
+
},
|
63
|
+
});
|
64
|
+
|
65
|
+
const onCountryChange = (v: CountryIso2) => {
|
66
|
+
if (!preview) {
|
67
|
+
setCountry(v);
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
if (preview) {
|
72
|
+
const isValid = phone && validatePhoneNumber(phone);
|
73
|
+
const canDial = allowDial && isValid;
|
74
|
+
|
75
|
+
return (
|
76
|
+
<Box
|
77
|
+
display="flex"
|
78
|
+
alignItems="center"
|
79
|
+
component={canDial ? 'a' : 'div'}
|
80
|
+
href={canDial ? `tel:${phone}` : undefined}
|
81
|
+
sx={{
|
82
|
+
textDecoration: 'none',
|
83
|
+
color: 'inherit',
|
84
|
+
cursor: canDial ? 'pointer' : 'default',
|
85
|
+
'&:hover': {
|
86
|
+
opacity: canDial ? 0.8 : 1,
|
87
|
+
},
|
88
|
+
}}>
|
89
|
+
<CountrySelect value={country} preview={preview} />
|
90
|
+
<Typography sx={{ ml: 0.5 }}>{phone}</Typography>
|
91
|
+
</Box>
|
92
|
+
);
|
93
|
+
}
|
94
|
+
return (
|
95
|
+
<TextField
|
96
|
+
{...props}
|
97
|
+
value={phone}
|
98
|
+
onChange={preview ? undefined : handlePhoneValueChange}
|
99
|
+
placeholder={placeholder}
|
100
|
+
className="phone-input"
|
101
|
+
disabled={preview}
|
102
|
+
sx={{
|
103
|
+
'&>.MuiInputBase-root': {
|
104
|
+
paddingLeft: '8px',
|
105
|
+
cursor: preview ? 'default' : undefined,
|
106
|
+
},
|
107
|
+
...(props.sx ?? {}),
|
108
|
+
...(preview
|
109
|
+
? {
|
110
|
+
'& .Mui-disabled': {
|
111
|
+
'-webkit-text-fill-color': 'inherit',
|
112
|
+
color: 'inherit',
|
113
|
+
},
|
114
|
+
fieldset: {
|
115
|
+
display: 'none',
|
116
|
+
},
|
117
|
+
}
|
118
|
+
: {}),
|
119
|
+
}}
|
120
|
+
InputProps={{
|
121
|
+
startAdornment: (
|
122
|
+
<InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
|
123
|
+
<CountrySelect
|
124
|
+
value={country}
|
125
|
+
onChange={onCountryChange}
|
126
|
+
preview={preview}
|
127
|
+
selectCountryProps={countryDisplayOptions}
|
128
|
+
sx={{
|
129
|
+
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
130
|
+
borderColor: 'transparent',
|
131
|
+
},
|
132
|
+
opacity: preview ? 1 : undefined,
|
133
|
+
cursor: preview ? 'default' : undefined,
|
134
|
+
}}
|
135
|
+
/>
|
136
|
+
</InputAdornment>
|
137
|
+
),
|
138
|
+
...(props.InputProps ?? {}),
|
139
|
+
}}
|
140
|
+
/>
|
141
|
+
);
|
142
|
+
}
|