@arcblock/ux 2.12.14 → 2.12.16
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/lib/Address/did-address.js +2 -2
- package/lib/Locale/selector.js +31 -31
- package/lib/PhoneInput/country-select.d.ts +13 -0
- package/lib/PhoneInput/country-select.js +142 -0
- package/lib/PhoneInput/index.d.ts +17 -0
- package/lib/PhoneInput/index.js +137 -0
- package/lib/SessionUser/components/un-login.js +2 -1
- package/lib/Util/constant.d.ts +1 -0
- package/lib/Util/constant.js +3 -3
- package/package.json +7 -5
- package/src/Address/did-address.tsx +1 -1
- package/src/Locale/selector.tsx +31 -32
- package/src/PhoneInput/country-select.tsx +138 -0
- package/src/PhoneInput/index.tsx +142 -0
- package/src/SessionUser/components/un-login.tsx +2 -1
- package/src/Util/constant.ts +3 -3
@@ -98,8 +98,8 @@ const DidAddress = /*#__PURE__*/forwardRef((props, ref) => {
|
|
98
98
|
/* title prop 直接加在 icon 上不生效 */
|
99
99
|
_jsx(CopyIcon, {
|
100
100
|
className: "did-address-copy",
|
101
|
-
width:
|
102
|
-
height:
|
101
|
+
width: "1em",
|
102
|
+
height: "1em",
|
103
103
|
onClick: onCopy
|
104
104
|
})
|
105
105
|
});
|
package/lib/Locale/selector.js
CHANGED
@@ -115,37 +115,37 @@ export default function LocaleSelector(props) {
|
|
115
115
|
children: languages.map(({
|
116
116
|
code,
|
117
117
|
name
|
118
|
-
}) =>
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
}]
|
147
|
-
}
|
148
|
-
}
|
118
|
+
}) => {
|
119
|
+
return /*#__PURE__*/_jsxs(MenuItem, {
|
120
|
+
className: "locale-item",
|
121
|
+
onClick: () => onSelect(code),
|
122
|
+
sx: {
|
123
|
+
fontSize: 16,
|
124
|
+
fontStyle: 'normal',
|
125
|
+
fontStretch: 'normal',
|
126
|
+
lineHeight: 'normal',
|
127
|
+
letterSpacing: '2px',
|
128
|
+
textAlign: 'center',
|
129
|
+
color: getColor({
|
130
|
+
...popperProps,
|
131
|
+
theme
|
132
|
+
}),
|
133
|
+
cursor: 'pointer',
|
134
|
+
display: 'flex',
|
135
|
+
padding: '16px',
|
136
|
+
alignItems: 'center'
|
137
|
+
},
|
138
|
+
children: [/*#__PURE__*/_jsx(Box, {
|
139
|
+
component: IconifyIcon,
|
140
|
+
icon: CheckIcon,
|
141
|
+
className: code === locale ? 'check-icon check-icon-visible' : 'check-icon',
|
142
|
+
sx: {
|
143
|
+
marginRight: 0.5,
|
144
|
+
visibility: code === locale ? 'visible' : 'hidden'
|
145
|
+
}
|
146
|
+
}), name]
|
147
|
+
}, code);
|
148
|
+
})
|
149
149
|
})
|
150
150
|
})
|
151
151
|
})
|
@@ -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
|
+
}
|
@@ -25,6 +25,7 @@ export default function UnLogin({
|
|
25
25
|
const t = useMemoizedFn((key, data = {}) => {
|
26
26
|
return translate(translations, key, locale, 'en', data);
|
27
27
|
});
|
28
|
+
const searchParams = new URLSearchParams(window.location.search);
|
28
29
|
const browser = useBrowser();
|
29
30
|
const isFirstLoading = false;
|
30
31
|
const userAnchorRef = useRef(null);
|
@@ -53,7 +54,7 @@ export default function UnLogin({
|
|
53
54
|
let timer;
|
54
55
|
|
55
56
|
// NOTICE: ArcSphere 中使用原生的快捷登录
|
56
|
-
if (!browser.arcSphere) {
|
57
|
+
if (!browser.arcSphere && searchParams.get('showQuickConnect') !== 'false') {
|
57
58
|
timer = setTimeout(async () => {
|
58
59
|
currentState.userSessions = await session.getUserSessions();
|
59
60
|
if (currentState.userSessions.length > 0) {
|
package/lib/Util/constant.d.ts
CHANGED
package/lib/Util/constant.js
CHANGED
@@ -15,7 +15,7 @@ export const PASSPORT_STATUS = {
|
|
15
15
|
REVOKED: 'revoked'
|
16
16
|
};
|
17
17
|
export const LOGIN_PROVIDER = {
|
18
|
-
|
18
|
+
EMAIL: 'email',
|
19
19
|
AUTH0: 'auth0',
|
20
20
|
APPLE: 'apple',
|
21
21
|
GITHUB: 'github',
|
@@ -25,8 +25,8 @@ export const LOGIN_PROVIDER = {
|
|
25
25
|
PASSKEY: 'passkey'
|
26
26
|
};
|
27
27
|
export const LOGIN_PROVIDER_NAME = {
|
28
|
-
|
29
|
-
[LOGIN_PROVIDER.AUTH0]: '
|
28
|
+
[LOGIN_PROVIDER.EMAIL]: 'Email',
|
29
|
+
[LOGIN_PROVIDER.AUTH0]: 'Auth0',
|
30
30
|
[LOGIN_PROVIDER.APPLE]: 'Apple',
|
31
31
|
[LOGIN_PROVIDER.GITHUB]: 'Github',
|
32
32
|
[LOGIN_PROVIDER.GOOGLE]: 'Google',
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.12.
|
3
|
+
"version": "2.12.16",
|
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": "02b619f243bf6a0c6847349edf513e18b212bcd7",
|
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.16",
|
75
|
+
"@arcblock/nft-display": "^2.12.16",
|
76
|
+
"@arcblock/react-hooks": "^2.12.16",
|
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
|
}
|
@@ -120,7 +120,7 @@ const DidAddress = forwardRef<HTMLDidAddressElement, IDidAddressProps>((props, r
|
|
120
120
|
</Tooltip>
|
121
121
|
) : (
|
122
122
|
/* title prop 直接加在 icon 上不生效 */
|
123
|
-
<CopyIcon className="did-address-copy" width=
|
123
|
+
<CopyIcon className="did-address-copy" width="1em" height="1em" onClick={onCopy} />
|
124
124
|
)}
|
125
125
|
</span>
|
126
126
|
);
|
package/src/Locale/selector.tsx
CHANGED
@@ -115,39 +115,38 @@ export default function LocaleSelector(props: LocaleSelectorProps) {
|
|
115
115
|
}}>
|
116
116
|
<ClickAwayListener onClickAway={onClose}>
|
117
117
|
<MenuList>
|
118
|
-
{languages.map(({ code, name }) =>
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
{
|
143
|
-
visibility: 'hidden',
|
118
|
+
{languages.map(({ code, name }) => {
|
119
|
+
return (
|
120
|
+
<MenuItem
|
121
|
+
key={code}
|
122
|
+
className="locale-item"
|
123
|
+
onClick={() => onSelect(code)}
|
124
|
+
sx={{
|
125
|
+
fontSize: 16,
|
126
|
+
fontStyle: 'normal',
|
127
|
+
fontStretch: 'normal',
|
128
|
+
lineHeight: 'normal',
|
129
|
+
letterSpacing: '2px',
|
130
|
+
textAlign: 'center',
|
131
|
+
color: getColor({ ...popperProps, theme }),
|
132
|
+
cursor: 'pointer',
|
133
|
+
display: 'flex',
|
134
|
+
padding: '16px',
|
135
|
+
alignItems: 'center',
|
136
|
+
}}>
|
137
|
+
<Box
|
138
|
+
component={IconifyIcon}
|
139
|
+
icon={CheckIcon}
|
140
|
+
className={code === locale ? 'check-icon check-icon-visible' : 'check-icon'}
|
141
|
+
sx={{
|
144
142
|
marginRight: 0.5,
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
143
|
+
visibility: code === locale ? 'visible' : 'hidden',
|
144
|
+
}}
|
145
|
+
/>
|
146
|
+
{name}
|
147
|
+
</MenuItem>
|
148
|
+
);
|
149
|
+
})}
|
151
150
|
</MenuList>
|
152
151
|
</ClickAwayListener>
|
153
152
|
</Box>
|
@@ -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
|
+
}
|
@@ -40,6 +40,7 @@ export default function UnLogin({ session, onLogin = noop, size = 24, dark = fal
|
|
40
40
|
const t = useMemoizedFn((key, data = {}) => {
|
41
41
|
return translate(translations, key, locale, 'en', data);
|
42
42
|
});
|
43
|
+
const searchParams = new URLSearchParams(window.location.search);
|
43
44
|
const browser = useBrowser();
|
44
45
|
|
45
46
|
const isFirstLoading = false;
|
@@ -74,7 +75,7 @@ export default function UnLogin({ session, onLogin = noop, size = 24, dark = fal
|
|
74
75
|
let timer: NodeJS.Timeout;
|
75
76
|
|
76
77
|
// NOTICE: ArcSphere 中使用原生的快捷登录
|
77
|
-
if (!browser.arcSphere) {
|
78
|
+
if (!browser.arcSphere && searchParams.get('showQuickConnect') !== 'false') {
|
78
79
|
timer = setTimeout(async () => {
|
79
80
|
currentState.userSessions = await session.getUserSessions();
|
80
81
|
if (currentState.userSessions.length > 0) {
|
package/src/Util/constant.ts
CHANGED
@@ -20,7 +20,7 @@ export const PASSPORT_STATUS = {
|
|
20
20
|
};
|
21
21
|
|
22
22
|
export const LOGIN_PROVIDER = {
|
23
|
-
|
23
|
+
EMAIL: 'email',
|
24
24
|
AUTH0: 'auth0',
|
25
25
|
APPLE: 'apple',
|
26
26
|
GITHUB: 'github',
|
@@ -31,8 +31,8 @@ export const LOGIN_PROVIDER = {
|
|
31
31
|
};
|
32
32
|
|
33
33
|
export const LOGIN_PROVIDER_NAME = {
|
34
|
-
|
35
|
-
[LOGIN_PROVIDER.AUTH0]: '
|
34
|
+
[LOGIN_PROVIDER.EMAIL]: 'Email',
|
35
|
+
[LOGIN_PROVIDER.AUTH0]: 'Auth0',
|
36
36
|
[LOGIN_PROVIDER.APPLE]: 'Apple',
|
37
37
|
[LOGIN_PROVIDER.GITHUB]: 'Github',
|
38
38
|
[LOGIN_PROVIDER.GOOGLE]: 'Google',
|