@arcblock/ux 2.12.21 → 2.12.23
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/NavMenu/nav-menu.js +11 -6
- package/lib/PhoneInput/country-select.d.ts +1 -1
- package/lib/PhoneInput/country-select.js +46 -37
- package/lib/PhoneInput/index.d.ts +0 -1
- package/lib/PhoneInput/index.js +104 -78
- package/lib/SessionBlocklet/index.d.ts +4 -2
- package/lib/SessionBlocklet/index.js +20 -4
- package/lib/SessionUser/components/logged-in.d.ts +4 -2
- package/lib/SessionUser/components/logged-in.js +20 -4
- package/lib/SessionUser/index.d.ts +2 -1
- package/lib/SessionUser/index.js +4 -2
- package/package.json +6 -6
- package/src/NavMenu/nav-menu.tsx +18 -22
- package/src/PhoneInput/country-select.tsx +41 -63
- package/src/PhoneInput/index.tsx +103 -80
- package/src/SessionBlocklet/index.tsx +21 -5
- package/src/SessionUser/components/logged-in.tsx +15 -5
- package/src/SessionUser/index.tsx +16 -2
package/lib/NavMenu/nav-menu.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import { Children, cloneElement, useEffect, useRef, forwardRef, useLayoutEffect, isValidElement } from 'react';
|
3
|
+
import { ClickAwayListener } from '@mui/material';
|
3
4
|
import clsx from 'clsx';
|
4
5
|
import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuIcon } from '@mui/icons-material';
|
5
6
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
@@ -300,8 +301,8 @@ export const Sub = /*#__PURE__*/forwardRef(({
|
|
300
301
|
'navmenu-item--inline': isInlineMode
|
301
302
|
}, rest.className);
|
302
303
|
|
303
|
-
//
|
304
|
-
const props =
|
304
|
+
// 统一使用 click 事件控制收缩/伸展子菜单
|
305
|
+
const props = {
|
305
306
|
onClick: () => {
|
306
307
|
if (openedIds?.includes(id)) {
|
307
308
|
close?.(id);
|
@@ -309,15 +310,12 @@ export const Sub = /*#__PURE__*/forwardRef(({
|
|
309
310
|
open?.(id);
|
310
311
|
}
|
311
312
|
}
|
312
|
-
} : {
|
313
|
-
onMouseEnter: () => open?.(id),
|
314
|
-
onMouseLeave: () => close?.(id)
|
315
313
|
};
|
316
314
|
// inline mode, 避免点击子菜单项时触发父菜单的 open/close
|
317
315
|
const containerProps = isInlineMode ? {
|
318
316
|
onClick: e => e.stopPropagation()
|
319
317
|
} : {};
|
320
|
-
|
318
|
+
const menu = /*#__PURE__*/_jsxs(NavMenuSub, {
|
321
319
|
...rest,
|
322
320
|
className: classes,
|
323
321
|
...props,
|
@@ -351,6 +349,13 @@ export const Sub = /*#__PURE__*/forwardRef(({
|
|
351
349
|
})
|
352
350
|
})]
|
353
351
|
});
|
352
|
+
if (!isInlineMode) {
|
353
|
+
return menu;
|
354
|
+
}
|
355
|
+
return /*#__PURE__*/_jsx(ClickAwayListener, {
|
356
|
+
onClickAway: () => close?.(id),
|
357
|
+
children: menu
|
358
|
+
});
|
354
359
|
});
|
355
360
|
Sub.displayName = 'NavMenu.Sub';
|
356
361
|
NavMenu.Item = Item;
|
@@ -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
|
+
};
|
63
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));
|
109
|
+
};
|
110
|
+
|
111
|
+
// 预览模式
|
64
112
|
if (preview) {
|
65
113
|
const isValid = phone && validatePhoneNumber(phone);
|
66
114
|
const canDial = allowDial && isValid;
|
@@ -90,56 +138,34 @@ 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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
display: 'none'
|
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
|
118
165
|
}
|
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
|
-
}
|
166
|
+
}, props.sx // 这里传入的可能是一个数组或一个对象
|
167
|
+
),
|
168
|
+
InputProps: props.InputProps ?? {}
|
169
|
+
})]
|
144
170
|
});
|
145
171
|
}
|
@@ -1,7 +1,9 @@
|
|
1
|
+
import { type BoxProps } from '@mui/material';
|
1
2
|
import type { Locale } from '../type';
|
2
|
-
export interface SessionBlockletProps {
|
3
|
+
export interface SessionBlockletProps extends Omit<BoxProps, 'onClick' | 'onMouseEnter' | 'onMouseLeave'> {
|
3
4
|
session: Record<string, any>;
|
4
5
|
locale?: Locale;
|
5
6
|
size?: number;
|
7
|
+
popperType?: 'hover' | 'click';
|
6
8
|
}
|
7
|
-
export default function SessionBlocklet({ session, locale, size }: SessionBlockletProps): import("react/jsx-runtime").JSX.Element | null;
|
9
|
+
export default function SessionBlocklet({ session, locale, size, popperType, sx, ...rest }: SessionBlockletProps): import("react/jsx-runtime").JSX.Element | null;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import { useRef } from 'react';
|
3
3
|
import { Box, ClickAwayListener, Fade, IconButton, List, ListItem, ListItemButton, Paper, Popper, Typography } from '@mui/material';
|
4
4
|
import { Icon } from '@iconify/react';
|
@@ -13,7 +13,10 @@ import { getTranslation } from '../Util';
|
|
13
13
|
export default function SessionBlocklet({
|
14
14
|
session,
|
15
15
|
locale = 'zh',
|
16
|
-
size = 24
|
16
|
+
size = 24,
|
17
|
+
popperType = 'click',
|
18
|
+
sx,
|
19
|
+
...rest
|
17
20
|
}) {
|
18
21
|
const blocklet = window?.blocklet || {};
|
19
22
|
const sessionMenuList = (blocklet.navigation || []
|
@@ -37,14 +40,27 @@ export default function SessionBlocklet({
|
|
37
40
|
const onTogglePopper = useMemoizedFn((value = !currentState.open) => {
|
38
41
|
currentState.open = value;
|
39
42
|
});
|
43
|
+
const handleEventProps = popperType === 'hover' ? {
|
44
|
+
onMouseEnter: () => onTogglePopper(true),
|
45
|
+
onMouseLeave: () => onTogglePopper(false)
|
46
|
+
} : {
|
47
|
+
onClick: () => onTogglePopper()
|
48
|
+
};
|
40
49
|
if (sessionMenuList.length === 0 || !session?.user) {
|
41
50
|
return null;
|
42
51
|
}
|
43
|
-
return /*#__PURE__*/_jsxs(
|
52
|
+
return /*#__PURE__*/_jsxs(Box, {
|
53
|
+
sx: {
|
54
|
+
display: 'inline-flex',
|
55
|
+
alignItems: 'center',
|
56
|
+
justifyContent: 'center',
|
57
|
+
...sx
|
58
|
+
},
|
59
|
+
...rest,
|
60
|
+
...handleEventProps,
|
44
61
|
children: [/*#__PURE__*/_jsx(IconButton, {
|
45
62
|
size: "medium",
|
46
63
|
ref: popperAnchorRef,
|
47
|
-
onClick: () => onTogglePopper(),
|
48
64
|
children: /*#__PURE__*/_jsx(Icon, {
|
49
65
|
icon: DashboardOutlineRoundedIcon,
|
50
66
|
fontSize: size,
|
@@ -1,10 +1,12 @@
|
|
1
|
+
import { type BoxProps } from '@mui/material';
|
1
2
|
import type { Locale, Session } from '../../type';
|
2
|
-
export interface LoggedInProps {
|
3
|
+
export interface LoggedInProps extends Omit<BoxProps, 'onClick' | 'onMouseEnter' | 'onMouseLeave'> {
|
3
4
|
session: Session;
|
4
5
|
dark?: false | true;
|
5
6
|
onBindWallet?: () => void;
|
6
7
|
isBlocklet?: true | false;
|
7
8
|
locale?: Locale;
|
8
9
|
size?: number;
|
10
|
+
popperType?: 'hover' | 'click';
|
9
11
|
}
|
10
|
-
export default function LoggedIn({ session, dark, onBindWallet, isBlocklet, locale, size, }: LoggedInProps): import("react/jsx-runtime").JSX.Element;
|
12
|
+
export default function LoggedIn({ session, dark, onBindWallet, isBlocklet, locale, size, popperType, sx, ...rest }: LoggedInProps): import("react/jsx-runtime").JSX.Element;
|
@@ -30,7 +30,10 @@ export default function LoggedIn({
|
|
30
30
|
onBindWallet = noop,
|
31
31
|
isBlocklet = true,
|
32
32
|
locale = 'en',
|
33
|
-
size = 24
|
33
|
+
size = 24,
|
34
|
+
popperType = 'click',
|
35
|
+
sx,
|
36
|
+
...rest
|
34
37
|
}) {
|
35
38
|
const t = useMemoizedFn((key, data = {}) => {
|
36
39
|
return translate(translations, key, locale, 'en', data);
|
@@ -45,6 +48,12 @@ export default function LoggedIn({
|
|
45
48
|
const onTogglePopper = useMemoizedFn((value = !currentState.open) => {
|
46
49
|
currentState.open = value;
|
47
50
|
});
|
51
|
+
const handleEventProps = popperType === 'hover' ? {
|
52
|
+
onMouseEnter: () => onTogglePopper(true),
|
53
|
+
onMouseLeave: () => onTogglePopper(false)
|
54
|
+
} : {
|
55
|
+
onClick: () => onTogglePopper()
|
56
|
+
};
|
48
57
|
|
49
58
|
// base64 img maybe have some blank char, need encodeURIComponent to transform it
|
50
59
|
const avatar = getUserAvatar(session.user?.avatar?.replace(/\s/g, encodeURIComponent(' ')));
|
@@ -97,10 +106,17 @@ export default function LoggedIn({
|
|
97
106
|
Copy(link);
|
98
107
|
Toast.success(t('inviteCopied'));
|
99
108
|
});
|
100
|
-
return /*#__PURE__*/_jsxs(
|
109
|
+
return /*#__PURE__*/_jsxs(Box, {
|
110
|
+
sx: {
|
111
|
+
display: 'inline-flex',
|
112
|
+
alignItems: 'center',
|
113
|
+
justifyContent: 'center',
|
114
|
+
...sx
|
115
|
+
},
|
116
|
+
...rest,
|
117
|
+
...handleEventProps,
|
101
118
|
children: [/*#__PURE__*/_jsx(IconButton, {
|
102
119
|
ref: popperAnchorRef,
|
103
|
-
onClick: () => onTogglePopper(),
|
104
120
|
size: "medium",
|
105
121
|
"data-cy": "sessionManager-logout-popup",
|
106
122
|
className: "arc-session-user-logged-in",
|
@@ -129,7 +145,7 @@ export default function LoggedIn({
|
|
129
145
|
onClickAway: e => {
|
130
146
|
e.preventDefault();
|
131
147
|
e.stopPropagation();
|
132
|
-
|
148
|
+
currentState.open = false;
|
133
149
|
},
|
134
150
|
children: /*#__PURE__*/_jsx(Fade, {
|
135
151
|
...TransitionProps,
|
@@ -4,5 +4,6 @@ export interface SessionUserProps {
|
|
4
4
|
onBindWallet?: () => void;
|
5
5
|
locale?: Locale;
|
6
6
|
size?: number;
|
7
|
+
popperType?: 'hover' | 'click';
|
7
8
|
}
|
8
|
-
export default function SessionUser({ session, onBindWallet, locale, size }: SessionUserProps): import("react/jsx-runtime").JSX.Element;
|
9
|
+
export default function SessionUser({ session, onBindWallet, locale, size, popperType, }: SessionUserProps): import("react/jsx-runtime").JSX.Element;
|
package/lib/SessionUser/index.js
CHANGED
@@ -7,7 +7,8 @@ export default function SessionUser({
|
|
7
7
|
session,
|
8
8
|
onBindWallet = noop,
|
9
9
|
locale = 'en',
|
10
|
-
size = 24
|
10
|
+
size = 24,
|
11
|
+
popperType = 'click'
|
11
12
|
}) {
|
12
13
|
const isBlocklet = useCreation(() => {
|
13
14
|
return !!globalThis?.blocklet;
|
@@ -18,7 +19,8 @@ export default function SessionUser({
|
|
18
19
|
session: session,
|
19
20
|
onBindWallet: onBindWallet,
|
20
21
|
locale: locale,
|
21
|
-
size: size
|
22
|
+
size: size,
|
23
|
+
popperType: popperType
|
22
24
|
});
|
23
25
|
}
|
24
26
|
return /*#__PURE__*/_jsx(UnLogin, {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.12.
|
3
|
+
"version": "2.12.23",
|
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": "13125f3eb13c42e5d0abbe6d3c33f5b315cfdd88",
|
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.23",
|
75
|
+
"@arcblock/nft-display": "^2.12.23",
|
76
|
+
"@arcblock/react-hooks": "^2.12.23",
|
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",
|
package/src/NavMenu/nav-menu.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Children, cloneElement, useEffect, useRef, forwardRef, useLayoutEffect, isValidElement } from 'react';
|
2
|
-
import { type SxProps } from '@mui/material';
|
2
|
+
import { ClickAwayListener, type SxProps } from '@mui/material';
|
3
3
|
import clsx from 'clsx';
|
4
4
|
import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuIcon } from '@mui/icons-material';
|
5
5
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
@@ -371,29 +371,19 @@ export const Sub = forwardRef<HTMLLIElement, SubProps>(
|
|
371
371
|
rest.className
|
372
372
|
);
|
373
373
|
|
374
|
-
//
|
375
|
-
const props =
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
open?.(id);
|
382
|
-
}
|
383
|
-
},
|
374
|
+
// 统一使用 click 事件控制收缩/伸展子菜单
|
375
|
+
const props = {
|
376
|
+
onClick: () => {
|
377
|
+
if (openedIds?.includes(id)) {
|
378
|
+
close?.(id);
|
379
|
+
} else {
|
380
|
+
open?.(id);
|
384
381
|
}
|
385
|
-
|
386
|
-
|
387
|
-
onMouseLeave: () => close?.(id),
|
388
|
-
};
|
382
|
+
},
|
383
|
+
};
|
389
384
|
// inline mode, 避免点击子菜单项时触发父菜单的 open/close
|
390
|
-
const containerProps = isInlineMode
|
391
|
-
|
392
|
-
onClick: (e: React.MouseEvent) => e.stopPropagation(),
|
393
|
-
}
|
394
|
-
: {};
|
395
|
-
|
396
|
-
return (
|
385
|
+
const containerProps = isInlineMode ? { onClick: (e: React.MouseEvent) => e.stopPropagation() } : {};
|
386
|
+
const menu = (
|
397
387
|
<NavMenuSub {...rest} className={classes} {...props} ref={ref} $activeTextColor={activeTextColor}>
|
398
388
|
{icon && <span className="navmenu-item__icon">{icon}</span>}
|
399
389
|
<div className="navmenu-item__content">
|
@@ -414,6 +404,12 @@ export const Sub = forwardRef<HTMLLIElement, SubProps>(
|
|
414
404
|
</SubContainer>
|
415
405
|
</NavMenuSub>
|
416
406
|
);
|
407
|
+
|
408
|
+
if (!isInlineMode) {
|
409
|
+
return menu;
|
410
|
+
}
|
411
|
+
|
412
|
+
return <ClickAwayListener onClickAway={() => close?.(id)}>{menu}</ClickAwayListener>;
|
417
413
|
}
|
418
414
|
);
|
419
415
|
Sub.displayName = 'NavMenu.Sub';
|
@@ -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,36 @@ 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
|
-
InputProps={{
|
130
|
-
startAdornment: (
|
131
|
-
<InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
|
132
|
-
<CountrySelect
|
133
|
-
value={country}
|
134
|
-
onChange={onCountryChange}
|
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
|
+
},
|
168
|
+
props.sx as any // 这里传入的可能是一个数组或一个对象
|
169
|
+
)}
|
170
|
+
InputProps={props.InputProps ?? {}}
|
171
|
+
/>
|
172
|
+
</Box>
|
150
173
|
);
|
151
174
|
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { useRef } from 'react';
|
2
2
|
import {
|
3
3
|
Box,
|
4
|
+
type BoxProps,
|
4
5
|
ClickAwayListener,
|
5
6
|
Fade,
|
6
7
|
IconButton,
|
@@ -23,13 +24,21 @@ import SessionPermission from '../SessionPermission';
|
|
23
24
|
import { getTranslation } from '../Util';
|
24
25
|
import type { Locale } from '../type';
|
25
26
|
|
26
|
-
export interface SessionBlockletProps {
|
27
|
+
export interface SessionBlockletProps extends Omit<BoxProps, 'onClick' | 'onMouseEnter' | 'onMouseLeave'> {
|
27
28
|
session: Record<string, any>;
|
28
29
|
locale?: Locale;
|
29
30
|
size?: number;
|
31
|
+
popperType?: 'hover' | 'click';
|
30
32
|
}
|
31
33
|
|
32
|
-
export default function SessionBlocklet({
|
34
|
+
export default function SessionBlocklet({
|
35
|
+
session,
|
36
|
+
locale = 'zh',
|
37
|
+
size = 24,
|
38
|
+
popperType = 'click',
|
39
|
+
sx,
|
40
|
+
...rest
|
41
|
+
}: SessionBlockletProps) {
|
33
42
|
const blocklet = window?.blocklet || {};
|
34
43
|
const sessionMenuList = (blocklet.navigation || [])
|
35
44
|
// HACK 过滤掉默认插入的 /sessionManager 菜单
|
@@ -54,14 +63,21 @@ export default function SessionBlocklet({ session, locale = 'zh', size = 24 }: S
|
|
54
63
|
const onTogglePopper = useMemoizedFn((value = !currentState.open) => {
|
55
64
|
currentState.open = value;
|
56
65
|
});
|
66
|
+
const handleEventProps =
|
67
|
+
popperType === 'hover'
|
68
|
+
? { onMouseEnter: () => onTogglePopper(true), onMouseLeave: () => onTogglePopper(false) }
|
69
|
+
: { onClick: () => onTogglePopper() };
|
57
70
|
|
58
71
|
if (sessionMenuList.length === 0 || !session?.user) {
|
59
72
|
return null;
|
60
73
|
}
|
61
74
|
|
62
75
|
return (
|
63
|
-
|
64
|
-
|
76
|
+
<Box
|
77
|
+
sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', ...sx }}
|
78
|
+
{...rest}
|
79
|
+
{...handleEventProps}>
|
80
|
+
<IconButton size="medium" ref={popperAnchorRef}>
|
65
81
|
<Icon icon={DashboardOutlineRoundedIcon} fontSize={size} style={{ transform: 'scale(1.08)' }} />
|
66
82
|
</IconButton>
|
67
83
|
<Popper
|
@@ -172,6 +188,6 @@ export default function SessionBlocklet({ session, locale = 'zh', size = 24 }: S
|
|
172
188
|
</ClickAwayListener>
|
173
189
|
)}
|
174
190
|
</Popper>
|
175
|
-
|
191
|
+
</Box>
|
176
192
|
);
|
177
193
|
}
|
@@ -38,13 +38,14 @@ const getInviteLink = (inviter: string) => {
|
|
38
38
|
return url.toString();
|
39
39
|
};
|
40
40
|
|
41
|
-
export interface LoggedInProps {
|
41
|
+
export interface LoggedInProps extends Omit<BoxProps, 'onClick' | 'onMouseEnter' | 'onMouseLeave'> {
|
42
42
|
session: Session;
|
43
43
|
dark?: false | true;
|
44
44
|
onBindWallet?: () => void;
|
45
45
|
isBlocklet?: true | false;
|
46
46
|
locale?: Locale;
|
47
47
|
size?: number;
|
48
|
+
popperType?: 'hover' | 'click';
|
48
49
|
}
|
49
50
|
|
50
51
|
export default function LoggedIn({
|
@@ -54,6 +55,9 @@ export default function LoggedIn({
|
|
54
55
|
isBlocklet = true,
|
55
56
|
locale = 'en',
|
56
57
|
size = 24,
|
58
|
+
popperType = 'click',
|
59
|
+
sx,
|
60
|
+
...rest
|
57
61
|
}: LoggedInProps) {
|
58
62
|
const t = useMemoizedFn((key, data = {}) => {
|
59
63
|
return translate(translations, key, locale, 'en', data);
|
@@ -68,6 +72,10 @@ export default function LoggedIn({
|
|
68
72
|
const onTogglePopper = useMemoizedFn((value = !currentState.open) => {
|
69
73
|
currentState.open = value;
|
70
74
|
});
|
75
|
+
const handleEventProps =
|
76
|
+
popperType === 'hover'
|
77
|
+
? { onMouseEnter: () => onTogglePopper(true), onMouseLeave: () => onTogglePopper(false) }
|
78
|
+
: { onClick: () => onTogglePopper() };
|
71
79
|
|
72
80
|
// base64 img maybe have some blank char, need encodeURIComponent to transform it
|
73
81
|
const avatar = getUserAvatar(session.user?.avatar?.replace(/\s/g, encodeURIComponent(' ')));
|
@@ -127,10 +135,12 @@ export default function LoggedIn({
|
|
127
135
|
});
|
128
136
|
|
129
137
|
return (
|
130
|
-
|
138
|
+
<Box
|
139
|
+
sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', ...sx }}
|
140
|
+
{...rest}
|
141
|
+
{...handleEventProps}>
|
131
142
|
<IconButton
|
132
143
|
ref={popperAnchorRef}
|
133
|
-
onClick={() => onTogglePopper()}
|
134
144
|
size="medium"
|
135
145
|
data-cy="sessionManager-logout-popup"
|
136
146
|
className="arc-session-user-logged-in"
|
@@ -151,7 +161,7 @@ export default function LoggedIn({
|
|
151
161
|
onClickAway={(e) => {
|
152
162
|
e.preventDefault();
|
153
163
|
e.stopPropagation();
|
154
|
-
|
164
|
+
currentState.open = false;
|
155
165
|
}}>
|
156
166
|
<Fade {...TransitionProps} timeout={350}>
|
157
167
|
<Paper
|
@@ -220,7 +230,7 @@ export default function LoggedIn({
|
|
220
230
|
</ClickAwayListener>
|
221
231
|
)}
|
222
232
|
</Popper>
|
223
|
-
|
233
|
+
</Box>
|
224
234
|
);
|
225
235
|
}
|
226
236
|
|
@@ -10,16 +10,30 @@ export interface SessionUserProps {
|
|
10
10
|
onBindWallet?: () => void;
|
11
11
|
locale?: Locale;
|
12
12
|
size?: number;
|
13
|
+
popperType?: 'hover' | 'click';
|
13
14
|
}
|
14
15
|
|
15
|
-
export default function SessionUser({
|
16
|
+
export default function SessionUser({
|
17
|
+
session,
|
18
|
+
onBindWallet = noop,
|
19
|
+
locale = 'en',
|
20
|
+
size = 24,
|
21
|
+
popperType = 'click',
|
22
|
+
}: SessionUserProps) {
|
16
23
|
const isBlocklet = useCreation(() => {
|
17
24
|
return !!globalThis?.blocklet;
|
18
25
|
}, []);
|
19
26
|
|
20
27
|
if (session.user) {
|
21
28
|
return (
|
22
|
-
<LoggedIn
|
29
|
+
<LoggedIn
|
30
|
+
isBlocklet={isBlocklet}
|
31
|
+
session={session}
|
32
|
+
onBindWallet={onBindWallet}
|
33
|
+
locale={locale}
|
34
|
+
size={size}
|
35
|
+
popperType={popperType}
|
36
|
+
/>
|
23
37
|
);
|
24
38
|
}
|
25
39
|
return <UnLogin session={session} locale={locale} size={size} />;
|