@blocklet/ui-react 2.12.8 → 2.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/@types/index.d.ts +34 -0
  2. package/lib/@types/index.js +16 -0
  3. package/lib/@types/shims.d.ts +1 -0
  4. package/lib/UserCenter/components/config-profile.js +23 -1
  5. package/lib/UserCenter/components/editable-field.d.ts +22 -0
  6. package/lib/UserCenter/components/editable-field.js +159 -0
  7. package/lib/UserCenter/components/nft.d.ts +4 -0
  8. package/lib/UserCenter/components/nft.js +93 -0
  9. package/lib/UserCenter/components/settings.js +32 -15
  10. package/lib/UserCenter/components/status-selector/duration-menu.d.ts +9 -0
  11. package/lib/UserCenter/components/status-selector/duration-menu.js +75 -0
  12. package/lib/UserCenter/components/status-selector/index.d.ts +9 -0
  13. package/lib/UserCenter/components/status-selector/index.js +39 -0
  14. package/lib/UserCenter/components/status-selector/menu-item.d.ts +24 -0
  15. package/lib/UserCenter/components/status-selector/menu-item.js +24 -0
  16. package/lib/UserCenter/components/user-center.js +119 -122
  17. package/lib/UserCenter/components/user-info/clock.d.ts +4 -0
  18. package/lib/UserCenter/components/user-info/clock.js +23 -0
  19. package/lib/UserCenter/components/user-info/link-preview-input.d.ts +5 -0
  20. package/lib/UserCenter/components/user-info/link-preview-input.js +181 -0
  21. package/lib/UserCenter/components/user-info/metadata.d.ts +7 -0
  22. package/lib/UserCenter/components/user-info/metadata.js +458 -0
  23. package/lib/UserCenter/components/user-info/switch-role.js +2 -3
  24. package/lib/UserCenter/components/user-info/user-basic-info.d.ts +2 -0
  25. package/lib/UserCenter/components/user-info/user-basic-info.js +159 -90
  26. package/lib/UserCenter/components/user-info/user-info.js +2 -16
  27. package/lib/UserCenter/components/user-info/user-status.d.ts +8 -0
  28. package/lib/UserCenter/components/user-info/user-status.js +153 -0
  29. package/lib/UserCenter/components/user-info/utils.d.ts +19 -0
  30. package/lib/UserCenter/components/user-info/utils.js +86 -0
  31. package/lib/UserCenter/libs/locales.d.ts +65 -0
  32. package/lib/UserCenter/libs/locales.js +67 -2
  33. package/lib/UserSessions/components/user-sessions.js +48 -14
  34. package/package.json +8 -5
  35. package/src/@types/index.ts +39 -0
  36. package/src/@types/shims.d.ts +1 -0
  37. package/src/UserCenter/components/config-profile.tsx +20 -1
  38. package/src/UserCenter/components/editable-field.tsx +180 -0
  39. package/src/UserCenter/components/nft.tsx +122 -0
  40. package/src/UserCenter/components/settings.tsx +16 -4
  41. package/src/UserCenter/components/status-selector/duration-menu.tsx +87 -0
  42. package/src/UserCenter/components/status-selector/index.tsx +52 -0
  43. package/src/UserCenter/components/status-selector/menu-item.tsx +52 -0
  44. package/src/UserCenter/components/user-center.tsx +104 -103
  45. package/src/UserCenter/components/user-info/clock.tsx +29 -0
  46. package/src/UserCenter/components/user-info/link-preview-input.tsx +227 -0
  47. package/src/UserCenter/components/user-info/metadata.tsx +465 -0
  48. package/src/UserCenter/components/user-info/switch-role.tsx +3 -3
  49. package/src/UserCenter/components/user-info/user-basic-info.tsx +150 -87
  50. package/src/UserCenter/components/user-info/user-info.tsx +6 -16
  51. package/src/UserCenter/components/user-info/user-status.tsx +182 -0
  52. package/src/UserCenter/components/user-info/utils.ts +114 -0
  53. package/src/UserCenter/libs/locales.ts +65 -0
  54. package/src/UserSessions/components/user-sessions.tsx +68 -18
@@ -0,0 +1,227 @@
1
+ import { Box, Button, FormControl, IconButton, TextField, Typography } from '@mui/material';
2
+ import styled from '@emotion/styled';
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
4
+ import RemoveIcon from '@arcblock/icons/lib/Subtract';
5
+ import AddIcon from '@mui/icons-material/Add';
6
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
7
+ import { ChangeEvent, useEffect, useMemo, useState } from 'react';
8
+ import { translate } from '@arcblock/ux/lib/Locale/util';
9
+ import { useMemoizedFn } from 'ahooks';
10
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
11
+ // eslint-disable-next-line import/no-extraneous-dependencies
12
+ import LinkIcon from '@arcblock/icons/lib/Link';
13
+ import { joinURL } from 'ufo';
14
+ import { isValidUrl } from './utils';
15
+ import { translations } from '../../libs/locales';
16
+
17
+ /**
18
+ * 链接预览输入框
19
+ */
20
+ function LinkInput({ value, onChange, error }: { value: string; onChange: (value: string) => void; error: boolean }) {
21
+ const handleUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
22
+ const inputValue = event.target.value;
23
+ onChange(inputValue);
24
+ };
25
+
26
+ return (
27
+ <FormControl fullWidth>
28
+ <TextField
29
+ variant="outlined"
30
+ value={value}
31
+ onChange={handleUrlChange}
32
+ fullWidth
33
+ error={error}
34
+ helperText={error ? 'Invalid URL' : ''}
35
+ sx={{
36
+ fieldset: {
37
+ borderColor: colors.dividerColor,
38
+ },
39
+ }}
40
+ />
41
+ </FormControl>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * 动态链接输入框
47
+ */
48
+ function DynamicLinkForm({ links = [], onChange }: { links: string[]; onChange: (links: string[]) => void }) {
49
+ const { locale } = useLocaleContext();
50
+ const t = useMemoizedFn((key, data = {}) => {
51
+ return translate(translations, key, locale, 'en', data);
52
+ });
53
+ const [errors, setErrors] = useState([false]); // 记录 URL 输入错误状态
54
+
55
+ const isLastError = useMemo(() => {
56
+ const lastLink = links[links.length - 1];
57
+ return !isValidUrl(lastLink) || (errors.length > 0 && errors[errors.length - 1]);
58
+ }, [errors, links]);
59
+
60
+ const handleAddLink = () => {
61
+ if (links.length < 5 || !isLastError) {
62
+ onChange([...links, '']);
63
+ }
64
+ };
65
+
66
+ // 删除指定索引的输入框
67
+ const handleRemoveLink = (index: number) => {
68
+ const updatedLinks = links.filter((_, i) => i !== index);
69
+ const updatedErrors = errors.filter((_, i) => i !== index);
70
+ setErrors(updatedErrors);
71
+ onChange(updatedLinks);
72
+ };
73
+
74
+ // 处理输入框变化
75
+ const handleInputChange = (index: number, value: string) => {
76
+ const updatedLinks = [...links];
77
+ updatedLinks[index] = value;
78
+
79
+ // 校验 URL 是否有效
80
+ const updatedErrors = [...errors];
81
+ updatedErrors[index] = !!(value && !isValidUrl(value)); // 如果有输入但格式错误,标记错误
82
+ setErrors(updatedErrors);
83
+ onChange(updatedLinks);
84
+ };
85
+
86
+ return (
87
+ <Box width="100%">
88
+ <Box display="flex" justifyContent="space-between" alignItems="center" gap={1}>
89
+ <Typography variant="subtitle1" gutterBottom sx={{ mb: 0, fontSize: '12px', color: '#4B5563' }}>
90
+ Social Media
91
+ </Typography>
92
+ </Box>
93
+ {links.map((link, index) => (
94
+ // eslint-disable-next-line react/no-array-index-key
95
+ <Box key={index} display="flex" alignItems="flex-start" mb={1}>
96
+ <LinkInput value={link} onChange={(value) => handleInputChange(index, value)} error={errors[index]} />
97
+ <IconButton sx={{ color: colors.foregroundsFgMuted }} onClick={() => handleRemoveLink(index)}>
98
+ <RemoveIcon />
99
+ </IconButton>
100
+ </Box>
101
+ ))}
102
+ {links.length < 5 && (
103
+ <Button
104
+ fullWidth
105
+ variant="outlined"
106
+ disabled={isLastError}
107
+ onClick={handleAddLink}
108
+ size="small"
109
+ sx={{
110
+ height: '40px',
111
+ border: `1px dashed ${colors.dividerColor}`,
112
+ color: colors.textBase,
113
+ '&:hover': {
114
+ border: `1px dashed ${colors.backgroundsBgSubtileHover}`,
115
+ backgroundColor: colors.backgroundsBgSubtileHover,
116
+ },
117
+ '&.Mui-disabled': {
118
+ borderStyle: 'dashed',
119
+ },
120
+ }}>
121
+ <AddIcon /> <span>{t('profile.addLink')}</span>
122
+ </Button>
123
+ )}
124
+ </Box>
125
+ );
126
+ }
127
+
128
+ /**
129
+ * favicon 预览
130
+ */
131
+ function FaviconPreview({ link }: { link: string }) {
132
+ const [favicon, setFavicon] = useState('');
133
+ const [iconError, setIconError] = useState(false); // 控制是否显示默认图标
134
+
135
+ useEffect(() => {
136
+ try {
137
+ if (link) {
138
+ const url = new URL(link);
139
+ const faviconUrl = joinURL(url.origin, 'favicon.ico');
140
+
141
+ // 尝试加载 Favicon
142
+ const img = new Image();
143
+ img.src = faviconUrl;
144
+ // TODO: CSP 导致解析失败
145
+ img.onload = () => {
146
+ setFavicon(faviconUrl);
147
+ setIconError(false);
148
+ };
149
+ img.onerror = () => {
150
+ setIconError(true);
151
+ setFavicon('');
152
+ };
153
+ } else {
154
+ setIconError(true);
155
+ setFavicon('');
156
+ }
157
+ } catch (error) {
158
+ setIconError(true); // URL 解析失败,直接回退到默认图标
159
+ }
160
+ }, [link]);
161
+
162
+ if (!link) {
163
+ return null;
164
+ }
165
+
166
+ if (iconError || !favicon) {
167
+ return <LinkIcon width={20} height={20} />;
168
+ }
169
+
170
+ return <img src={favicon} alt="Favicon" width={20} height={20} />;
171
+ }
172
+
173
+ export function LinkPreviewInput({
174
+ editable = false,
175
+ links = [],
176
+ onChange,
177
+ }: {
178
+ editable?: boolean;
179
+ links: string[];
180
+ onChange: (links: string[]) => void;
181
+ }) {
182
+ if (editable) {
183
+ return <DynamicLinkForm links={links} onChange={onChange} />;
184
+ }
185
+
186
+ return (
187
+ <Box width="100%" display="flex" flexDirection="column" gap={2}>
188
+ {links.map((link) => (
189
+ <Box
190
+ key={link}
191
+ display="flex"
192
+ alignItems="center"
193
+ gap={1}
194
+ sx={{
195
+ overflow: 'hidden',
196
+ width: '100%',
197
+ }}>
198
+ <FaviconPreview link={link} />
199
+ <LinkDiv>
200
+ <Box component="a" href={link} style={{ textDecoration: 'none' }} target="_blank" rel="noopener noreferrer">
201
+ {link}
202
+ </Box>
203
+ </LinkDiv>
204
+ </Box>
205
+ ))}
206
+ </Box>
207
+ );
208
+ }
209
+
210
+ const LinkDiv = styled.span`
211
+ flex: 1;
212
+ white-space: nowrap;
213
+ overflow: hidden;
214
+ text-overflow: ellipsis;
215
+ &,
216
+ & > * {
217
+ color: #222;
218
+ }
219
+
220
+ & > * {
221
+ word-break: break-all;
222
+ }
223
+
224
+ .status {
225
+ margin-left: 8px;
226
+ }
227
+ `;
@@ -0,0 +1,465 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ /* eslint-disable import/no-extraneous-dependencies */
3
+
4
+ import Box from '@mui/material/Box';
5
+ import MenuItem from '@mui/material/MenuItem';
6
+ import Select, { SelectChangeEvent } from '@mui/material/Select';
7
+ import useMediaQuery from '@mui/material/useMediaQuery';
8
+ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
9
+ import Backdrop, { BackdropProps } from '@mui/material/Backdrop';
10
+ import styled from '@emotion/styled';
11
+ import { joinURL } from 'ufo';
12
+ import Button from '@arcblock/ux/lib/Button';
13
+ import cloneDeep from 'lodash/cloneDeep';
14
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
15
+ import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
16
+ import { useMemo, useRef, useState, memo, forwardRef, useEffect, lazy } from 'react';
17
+ import { translate } from '@arcblock/ux/lib/Locale/util';
18
+ import isEmail from 'validator/lib/isEmail';
19
+ import isMobilePhone from 'validator/lib/isMobilePhone';
20
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
21
+
22
+ import { useBrowser } from '@arcblock/react-hooks';
23
+ import { translations } from '../../libs/locales';
24
+ import type { User, UserMetadata } from '../../../@types';
25
+ import EditableField from '../editable-field';
26
+ import { LinkPreviewInput } from './link-preview-input';
27
+ import { getTimezones } from './utils';
28
+ import Clock from './clock';
29
+
30
+ const LocationIcon = lazy(() => import('@arcblock/icons/lib/Location'));
31
+ const TimezoneIcon = lazy(() => import('@arcblock/icons/lib/Timezone'));
32
+ const EmailIcon = lazy(() => import('@arcblock/icons/lib/Email'));
33
+ const PhoneIcon = lazy(() => import('@arcblock/icons/lib/Phone'));
34
+ const ArrowDownwardIcon = lazy(() => import('@arcblock/icons/lib/ArrowDown'));
35
+
36
+ const defaultButtonStyle = {
37
+ color: colors.foregroundsFgBase,
38
+ borderColor: colors.strokeBorderBase,
39
+ backgroundColor: colors.buttonsButtonNeutral,
40
+ '&:hover': {
41
+ borderColor: colors.strokeBorderBase,
42
+ backgroundColor: colors.buttonsButtonNeutralHover,
43
+ },
44
+ py: 0.5,
45
+ borderRadius: 2,
46
+ };
47
+
48
+ const primaryButtonStyle = {
49
+ color: colors.buttonsButtonNeutral,
50
+ borderColor: colors.foregroundsFgInteractive,
51
+ backgroundColor: colors.foregroundsFgInteractive,
52
+ '&:hover': {
53
+ borderColor: colors.foregroundsFgInteractive,
54
+ backgroundColor: colors.foregroundsFgInteractive,
55
+ },
56
+ py: 0.5,
57
+ borderRadius: 2,
58
+ };
59
+
60
+ const iconSize = {
61
+ width: 20,
62
+ height: 20,
63
+ };
64
+
65
+ const bioMaxLength = 200;
66
+
67
+ const BackdropWrap = memo(
68
+ forwardRef<HTMLDivElement, BackdropProps>(function BackdropWrap(backdropProps, ref) {
69
+ return (
70
+ <Backdrop
71
+ ref={ref}
72
+ component="div"
73
+ style={{
74
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
75
+ backdropFilter: 'blur(3px)',
76
+ touchAction: 'none',
77
+ }}
78
+ {...backdropProps}
79
+ key="background"
80
+ />
81
+ );
82
+ })
83
+ );
84
+
85
+ // 添加 displayName
86
+ BackdropWrap.displayName = 'BackdropWrap';
87
+
88
+ export default function UserMetadataComponent({
89
+ isMyself,
90
+ user,
91
+ onSave,
92
+ }: {
93
+ isMyself: boolean;
94
+ user: User;
95
+ onSave: (v: UserMetadata) => void;
96
+ isMobile?: boolean;
97
+ }) {
98
+ const [editable, setEditable] = useState(false);
99
+ const [visible, setVisible] = useState(false);
100
+ const drawerDragger = useRef(null);
101
+ const browser = useBrowser();
102
+ const isMobileView = useMediaQuery('(max-width:640px)') || browser.mobile.any;
103
+ const validateMsg = useReactive<Record<string, string>>({
104
+ email: '',
105
+ phone: '',
106
+ });
107
+
108
+ useEffect(() => {
109
+ if (!isMobileView) {
110
+ setVisible(false);
111
+ }
112
+ }, [isMobileView]);
113
+
114
+ const phoneVerified = useCreation(() => {
115
+ return user?.phoneVerified ?? false;
116
+ }, [user?.phoneVerified]);
117
+
118
+ const emailVerified = useCreation(() => {
119
+ return user?.emailVerified ?? false;
120
+ }, [user?.emailVerified]);
121
+
122
+ const { locale } = useLocaleContext();
123
+ const t = useMemoizedFn((key, data = {}) => {
124
+ return translate(translations, key, locale, 'en', data);
125
+ });
126
+
127
+ const metadata: UserMetadata = useReactive(
128
+ user?.metadata
129
+ ? cloneDeep(user.metadata)
130
+ : {
131
+ joinedAt: user?.createdAt,
132
+ email: user?.email,
133
+ phone: user?.phone,
134
+ }
135
+ );
136
+
137
+ const onChange = (v: any, field: keyof UserMetadata | 'email') => {
138
+ metadata[field] = v;
139
+ };
140
+
141
+ const timezones = getTimezones();
142
+
143
+ const onEdit = () => {
144
+ if (!isMobileView) {
145
+ setEditable(true);
146
+ } else {
147
+ setVisible(true);
148
+ }
149
+ };
150
+
151
+ const onCancel = () => {
152
+ const defaultMetadata: UserMetadata = cloneDeep(user?.metadata) ?? {};
153
+ if (defaultMetadata) {
154
+ Object.keys(metadata).forEach((key) => {
155
+ const k = key as keyof UserMetadata;
156
+ (metadata[k] as any) = defaultMetadata[k];
157
+ });
158
+ }
159
+ validateMsg.email = '';
160
+ validateMsg.phone = '';
161
+ if (!isMobileView) {
162
+ setEditable(false);
163
+ } else {
164
+ setVisible(false);
165
+ }
166
+ };
167
+
168
+ const links = useMemo(() => {
169
+ const defaultLinks = metadata?.links?.map((link) => link.url) || [''];
170
+ return defaultLinks.length > 0 ? defaultLinks : [''];
171
+ }, [metadata.links]);
172
+
173
+ const handleLinksChange = (values: string[]) => {
174
+ const newLinks = values.map((link) => {
175
+ if (!link) {
176
+ return {
177
+ url: link,
178
+ };
179
+ }
180
+ const targetLink = metadata.links?.find((l) => l.url === link);
181
+ if (targetLink) {
182
+ return {
183
+ ...targetLink,
184
+ url: link,
185
+ };
186
+ }
187
+ return {
188
+ url: link,
189
+ favicon: joinURL(link, 'favicon.ico'),
190
+ };
191
+ });
192
+ onChange(newLinks, 'links');
193
+ };
194
+
195
+ const handleSave = () => {
196
+ Object.keys(metadata).forEach((key) => {
197
+ const k = key as keyof UserMetadata;
198
+ const value = metadata[k];
199
+ if (value && typeof value === 'string') {
200
+ (metadata[k] as string) = value.trim();
201
+ }
202
+ if (k === 'bio') {
203
+ metadata[k] = metadata[k]?.slice(0, bioMaxLength);
204
+ }
205
+ });
206
+
207
+ onSave(metadata);
208
+ setEditable(false);
209
+ setVisible(false);
210
+ };
211
+
212
+ const renderEdit = (editing: boolean, mode: 'drawer' | 'self' = 'self') => {
213
+ return (
214
+ <MetadataInfo pt={2} display="flex" flexDirection="column">
215
+ <EditableField
216
+ value={metadata.bio ?? ''}
217
+ onChange={(value) => onChange(value, 'bio')}
218
+ editable={editing}
219
+ placeholder="Bio"
220
+ component="textarea"
221
+ inline={false}
222
+ rows={3}
223
+ maxLength={bioMaxLength}
224
+ style={{
225
+ ...(editing ? { marginBottom: 8 } : {}),
226
+ }}
227
+ />
228
+ {!editing && isMyself ? (
229
+ <Button
230
+ size="large"
231
+ variant="outlined"
232
+ sx={{
233
+ ...defaultButtonStyle,
234
+ mb: 2,
235
+ mt: 2,
236
+ height: '40px',
237
+ }}
238
+ onClick={onEdit}
239
+ fullWidth>
240
+ {t('profile.editProfile')}
241
+ </Button>
242
+ ) : null}
243
+ <EditableField
244
+ value={metadata.location ?? ''}
245
+ onChange={(value) => onChange(value, 'location')}
246
+ editable={editing}
247
+ placeholder="Location"
248
+ label={t('profile.location')}
249
+ icon={<LocationIcon {...iconSize} />}
250
+ />
251
+
252
+ <EditableField
253
+ value={metadata.timezone ?? ''}
254
+ onChange={(value) => onChange(value, 'timezone')}
255
+ editable={editing}
256
+ placeholder="timezone"
257
+ icon={<TimezoneIcon {...iconSize} />}
258
+ label={t('profile.timezone')}
259
+ tooltip={
260
+ <p style={{ display: 'flex', margin: 0 }}>
261
+ <span style={{ marginRight: '4px' }}>{t('profile.localTime')}</span>
262
+ <Clock timezone={metadata.timezone} locale={locale} />
263
+ </p>
264
+ }>
265
+ <Select
266
+ className={`timezone-select ${editing ? '' : 'disabled'}`}
267
+ value={metadata.timezone}
268
+ onChange={(e: SelectChangeEvent) => onChange(e.target.value, 'timezone')}
269
+ disabled={!editing}
270
+ displayEmpty
271
+ variant="outlined"
272
+ placeholder="Timezone"
273
+ IconComponent={(props) => <ArrowDownwardIcon {...props} width={20} height={20} />}
274
+ MenuProps={{
275
+ style: {
276
+ zIndex: mode === 'drawer' ? 9999 : 1300,
277
+ },
278
+ }}
279
+ sx={{
280
+ width: '100%',
281
+
282
+ '&:hover': {
283
+ 'fieldset.MuiOutlinedInput-notchedOutline': {
284
+ borderColor: colors.dividerColor,
285
+ },
286
+ },
287
+ fieldset: {
288
+ borderColor: colors.dividerColor,
289
+ },
290
+ }}>
291
+ {timezones.map((tz) => (
292
+ <MenuItem key={tz.value} value={tz.value}>
293
+ {tz.label}
294
+ </MenuItem>
295
+ ))}
296
+ </Select>
297
+ </EditableField>
298
+
299
+ <EditableField
300
+ value={metadata.email ?? user?.email ?? ''}
301
+ editable={editing && !emailVerified}
302
+ verified={emailVerified}
303
+ placeholder="Email"
304
+ icon={<EmailIcon {...iconSize} />}
305
+ label={t('profile.email')}
306
+ onChange={(value) => onChange(value, 'email')}
307
+ errorMsg={validateMsg.email}
308
+ onValueValidate={(value) => {
309
+ let msg = '';
310
+ if (!!value && !isEmail(value)) {
311
+ msg = t('profile.emailInvalid');
312
+ }
313
+ validateMsg.email = msg;
314
+ }}
315
+ />
316
+
317
+ <EditableField
318
+ value={metadata.phone ?? user?.phone ?? ''}
319
+ editable={editing && !phoneVerified}
320
+ verified={phoneVerified}
321
+ placeholder="Phone"
322
+ icon={<PhoneIcon {...iconSize} />}
323
+ onChange={(value) => onChange(value, 'phone')}
324
+ label={t('profile.phone')}
325
+ errorMsg={validateMsg.phone}
326
+ onValueValidate={(value) => {
327
+ let msg = '';
328
+ if (!!value && !isMobilePhone(value)) {
329
+ msg = t('profile.phoneInvalid');
330
+ }
331
+ validateMsg.phone = msg;
332
+ }}
333
+ />
334
+
335
+ <LinkPreviewInput editable={editing} links={links} onChange={handleLinksChange} />
336
+ {editing && isMyself ? (
337
+ <Box
338
+ display="flex"
339
+ gap={1}
340
+ style={{ width: '100%' }}
341
+ justifyContent="flex-end"
342
+ flexDirection={mode === 'drawer' ? 'column' : 'row'}>
343
+ <Button
344
+ fullWidth={mode === 'drawer'}
345
+ size="small"
346
+ variant="outlined"
347
+ sx={{ ...defaultButtonStyle, minWidth: '54px' }}
348
+ onClick={onCancel}>
349
+ {t('common.cancel')}
350
+ </Button>
351
+ <Button
352
+ fullWidth={mode === 'drawer'}
353
+ size="small"
354
+ disabled={!!validateMsg.email || !!validateMsg.phone}
355
+ variant="outlined"
356
+ sx={{
357
+ ...primaryButtonStyle,
358
+ minWidth: '54px',
359
+ '&.Mui-disabled': {
360
+ backgroundColor: 'rgba(0, 0, 0, 0.12)',
361
+ },
362
+ }}
363
+ onClick={handleSave}>
364
+ {t('common.save')}
365
+ </Button>
366
+ </Box>
367
+ ) : null}
368
+ </MetadataInfo>
369
+ );
370
+ };
371
+
372
+ return (
373
+ <>
374
+ {renderEdit(editable)}
375
+ {isMobileView && (
376
+ <SwipeableDrawer
377
+ sx={{
378
+ zIndex: 9999,
379
+ }}
380
+ disableSwipeToOpen
381
+ onOpen={onEdit}
382
+ open={visible}
383
+ anchor="bottom"
384
+ onClose={onCancel}
385
+ slots={{
386
+ backdrop: BackdropWrap,
387
+ }}
388
+ PaperProps={{
389
+ sx: {
390
+ boxShadow: '0px -2px 16px 0px rgba(0, 0, 0, 0.08)',
391
+ borderRadius: 3, // 保持跟 DID Wallet 一致
392
+ borderBottomLeftRadius: 0,
393
+ borderBottomRightRadius: 0,
394
+ },
395
+ }}>
396
+ <Box
397
+ ref={drawerDragger}
398
+ sx={{
399
+ padding: '16px 32px',
400
+ margin: '-8px auto -16px',
401
+ zIndex: 1,
402
+ }}>
403
+ <Box
404
+ sx={{
405
+ width: '48px',
406
+ height: '4px',
407
+ borderRadius: '100vw',
408
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
409
+ }}
410
+ />
411
+ </Box>
412
+ <Box
413
+ p={2}
414
+ sx={{
415
+ maxHeight: '500px',
416
+ overflowY: 'auto',
417
+ }}>
418
+ {renderEdit(true, 'drawer')}
419
+ </Box>
420
+ </SwipeableDrawer>
421
+ )}
422
+ </>
423
+ );
424
+ }
425
+ const MetadataInfo = styled(Box)`
426
+ gap: 16px;
427
+ justify-content: space-between;
428
+ align-items: flex-start;
429
+ width: 100%;
430
+
431
+ .MuiOutlinedInput-root {
432
+ padding: 8px;
433
+ .MuiOutlinedInput-input {
434
+ padding: 0;
435
+ }
436
+ }
437
+ .timezone-select {
438
+ min-width: 150px;
439
+ &.disabled {
440
+ padding: 4px 8px;
441
+ fieldset {
442
+ border: unset;
443
+ }
444
+ svg {
445
+ display: none;
446
+ }
447
+ }
448
+ }
449
+ .info-row {
450
+ display: flex;
451
+ flex-direction: row;
452
+ align-items: center;
453
+ justify-content: center;
454
+ gap: 4px;
455
+ margin: 0;
456
+ p {
457
+ display: flex;
458
+ align-items: center;
459
+ margin: 0;
460
+ font-size: 16px;
461
+ font-weight: 400;
462
+ color: #666;
463
+ }
464
+ }
465
+ `;
@@ -1,8 +1,8 @@
1
1
  import { memo } from 'react';
2
2
  import { Chip } from '@mui/material';
3
- import { Icon } from '@iconify/react';
4
3
  import { useCreation } from 'ahooks';
5
- import SwapHorizRoundedIcon from '@iconify-icons/material-symbols/swap-horiz-rounded';
4
+ // eslint-disable-next-line import/no-extraneous-dependencies
5
+ import SwitchIcon from '@arcblock/icons/lib/Switch';
6
6
  import { temp as colors } from '@arcblock/ux/lib/Colors';
7
7
  import type { User } from '../../../@types';
8
8
 
@@ -34,7 +34,7 @@ function SwitchRole({ user, switchPassport }: { user: User; switchPassport: () =
34
34
  },
35
35
  }}
36
36
  clickable
37
- deleteIcon={<Icon icon={SwapHorizRoundedIcon} color={colors.textBase} />}
37
+ deleteIcon={<SwitchIcon color={colors.textBase} width={20} height={20} />}
38
38
  onDelete={switchPassport}
39
39
  onClick={switchPassport}
40
40
  />