@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,122 @@
1
+ import { Box, Pagination, Skeleton, Typography } from '@mui/material';
2
+ import { useCreation, useMemoizedFn, useReactive, useRequest } from 'ahooks';
3
+ import axios from 'axios';
4
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
5
+ import NFTDisplay from '@arcblock/ux/lib/NFTDisplay';
6
+ import Empty from '@arcblock/ux/lib/Empty';
7
+ import { WELLKNOWN_SERVICE_PATH_PREFIX } from '@abtnode/constant';
8
+ import { translate } from '@arcblock/ux/lib/Locale/util';
9
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
10
+ import { translations } from '../libs/locales';
11
+ import { User } from '../../@types';
12
+
13
+ interface NftInfo {
14
+ address: string;
15
+ data: Record<string, any>;
16
+ display: Record<string, any>;
17
+ issuer: string;
18
+ }
19
+
20
+ interface ResponseData {
21
+ assets: NftInfo[];
22
+ code: string;
23
+ page: { cursor: string; next: boolean; total: number };
24
+ }
25
+
26
+ interface PaginationProps {
27
+ page: number;
28
+ size: number;
29
+ }
30
+
31
+ export default function Nft({ user }: { user: User }) {
32
+ const { locale } = useLocaleContext();
33
+ const t = useMemoizedFn((key, data = {}) => {
34
+ return translate(translations, key, locale, 'en', data);
35
+ });
36
+ const paging = useReactive<PaginationProps>({
37
+ page: 1,
38
+ size: 20,
39
+ });
40
+ const userState = useRequest<ResponseData, [PaginationProps]>(
41
+ async (pagination: PaginationProps = paging) => {
42
+ const response = await axios.get(`${WELLKNOWN_SERVICE_PATH_PREFIX}/ocap/listAssets`, {
43
+ params: {
44
+ ownerAddress: user.did,
45
+ ...pagination,
46
+ },
47
+ });
48
+ return response.data;
49
+ },
50
+ {
51
+ defaultParams: [paging],
52
+ refreshDeps: [user.did, paging],
53
+ }
54
+ );
55
+
56
+ const { loading, data } = userState;
57
+
58
+ const dataPage = data?.page ?? { cursor: 0, next: false, total: 0 };
59
+
60
+ const handlePageChange = (event: React.ChangeEvent<unknown>, value: number) => {
61
+ paging.page = value;
62
+ userState.run(paging);
63
+ };
64
+
65
+ const content = useCreation(() => {
66
+ if (loading) {
67
+ return (
68
+ <Box display="flex" flexDirection="column" gap={2}>
69
+ <Skeleton width="20%" />
70
+ <Skeleton variant="rectangular" height={200} sx={{ borderRadius: 2 }} />
71
+ </Box>
72
+ );
73
+ }
74
+ return (
75
+ <>
76
+ <Typography
77
+ sx={{
78
+ color: colors.foregroundsFgBase,
79
+ fontWeight: 600,
80
+ mb: 2.5,
81
+ }}>
82
+ {t('common.nft')}
83
+ </Typography>
84
+ <Box className="nft-list-wrapper" display="flex" flexDirection="row" gap={2}>
85
+ {data?.assets?.map((item) => (
86
+ <Box key={item.address} width={166} height={166}>
87
+ <NFTDisplay
88
+ data={item.display}
89
+ address={item.address}
90
+ inset
91
+ imageFilter={{
92
+ imageFilter: 'resize',
93
+ w: '500',
94
+ f: 'webp',
95
+ }}
96
+ />
97
+ </Box>
98
+ ))}
99
+ {data?.assets?.length === 0 && !loading && (
100
+ <Box display="flex" justifyContent="center" alignItems="center" width="100%" height="100%">
101
+ <Empty>{t('common.noNFT')}</Empty>
102
+ </Box>
103
+ )}
104
+ </Box>
105
+ {dataPage.next || paging.page > 1 ? (
106
+ <Pagination
107
+ sx={{
108
+ display: 'flex',
109
+ justifyContent: 'end',
110
+ }}
111
+ page={paging.page}
112
+ onChange={handlePageChange}
113
+ count={Math.ceil(dataPage.total / paging.size)}
114
+ size="small"
115
+ />
116
+ ) : null}
117
+ </>
118
+ );
119
+ }, [loading, dataPage, paging.page, paging.size, handlePageChange]);
120
+
121
+ return <Box sx={{ border: `1px solid ${colors.dividerColor}`, borderRadius: 2, p: 2, mb: 5 }}>{content}</Box>;
122
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect } from 'react';
2
- import { Box, Divider, Typography } from '@mui/material';
2
+ import { Box, Typography } from '@mui/material';
3
3
  import type { BoxProps } from '@mui/material';
4
4
  import { useCreation, useMemoizedFn } from 'ahooks';
5
5
  import { translate } from '@arcblock/ux/lib/Locale/util';
@@ -85,13 +85,26 @@ export default function Settings({
85
85
  {...rest}
86
86
  sx={{
87
87
  ...rest?.sx,
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ gap: 2.5,
88
91
  minWidth: {
89
92
  md: 500,
90
93
  },
91
94
  maxWidth: '100%',
92
95
  }}>
93
- {tabs.map((tab, index) => (
94
- <Box id={tab.value} key={tab.value}>
96
+ {tabs.map((tab) => (
97
+ <Box
98
+ id={tab.value}
99
+ key={tab.value}
100
+ sx={{
101
+ border: `1px solid ${colors.dividerColor}`,
102
+ borderRadius: 2,
103
+ p: 2,
104
+ '&:last-child': {
105
+ mb: 5,
106
+ },
107
+ }}>
95
108
  <Typography
96
109
  sx={{
97
110
  color: colors.foregroundsFgBase,
@@ -100,7 +113,6 @@ export default function Settings({
100
113
  {tab.label}
101
114
  </Typography>
102
115
  <Box mt={2.5}>{tab.content}</Box>
103
- {index < tabs.length - 1 ? <Divider sx={{ mt: 2.5, mb: 2.5, borderColor: colors.dividerColor }} /> : null}
104
116
  </Box>
105
117
  ))}
106
118
  </Box>
@@ -0,0 +1,87 @@
1
+ import { Menu, Typography } from '@mui/material';
2
+ import styled from '@emotion/styled';
3
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
4
+ import { useState } from 'react';
5
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
6
+ import { useMemoizedFn } from 'ahooks';
7
+ import { translate } from '@arcblock/ux/lib/Locale/util';
8
+ import { translations } from '../../libs/locales';
9
+ import { DurationEnum } from '../../../@types';
10
+ import StatusMenuItem, { BaseStatusProps, StatusItem, StyledMenu } from './menu-item';
11
+
12
+ interface StatusDurationMenuProps extends BaseStatusProps {
13
+ data: StatusItem;
14
+ }
15
+ function DurationMenu({ data, selected, onSelect }: StatusDurationMenuProps) {
16
+ const { locale } = useLocaleContext();
17
+ const t = useMemoizedFn((key, _data = {}) => {
18
+ return translate(translations, key, locale, 'en', _data);
19
+ });
20
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
21
+
22
+ const open = Boolean(anchorEl);
23
+
24
+ const openSubMenu = (e: React.MouseEvent<HTMLElement>) => {
25
+ setAnchorEl(e.currentTarget);
26
+ };
27
+
28
+ const closeSubMenu = () => {
29
+ setAnchorEl(null);
30
+ };
31
+
32
+ const onSelectStatus = (e: React.MouseEvent<HTMLElement>) => {
33
+ openSubMenu(e);
34
+ };
35
+
36
+ const onSelectDuration = (e: React.MouseEvent<HTMLElement>, item: StatusItem) => {
37
+ onSelect({
38
+ ...(selected ?? {}),
39
+ value: data.id,
40
+ duration: item.id as DurationEnum,
41
+ });
42
+ closeSubMenu();
43
+ };
44
+ return (
45
+ <>
46
+ <StatusMenuItem icon={data.icon} selected={selected?.value === data.id} onClick={onSelectStatus}>
47
+ {data.name}
48
+ </StatusMenuItem>
49
+
50
+ <StyledMenu
51
+ anchorOrigin={{
52
+ vertical: 'top',
53
+ horizontal: 'right',
54
+ }}
55
+ transformOrigin={{
56
+ vertical: 'top',
57
+ horizontal: 'left',
58
+ }}
59
+ open={open}
60
+ onClose={closeSubMenu}
61
+ anchorEl={anchorEl}>
62
+ <Typography component="span" color="text.secondary" pl={2} fontSize="14px">
63
+ {t('profile.removeStatusAfter')}
64
+ </Typography>
65
+ {data.children?.map((item) => (
66
+ <StatusMenuItem
67
+ key={item.id}
68
+ selected={selected?.duration === item.id}
69
+ onClick={(e) => onSelectDuration(e, item)}>
70
+ {item.name}
71
+ </StatusMenuItem>
72
+ ))}
73
+ </StyledMenu>
74
+ </>
75
+ );
76
+ }
77
+
78
+ export default DurationMenu;
79
+
80
+ export const MenuDiv = styled(Menu)`
81
+ .MuiList-root {
82
+ min-width: 160px;
83
+ }
84
+ .selected {
85
+ background-color: ${colors.backgroundsBgSubtitle};
86
+ }
87
+ `;
@@ -0,0 +1,52 @@
1
+ import { PopoverProps, Typography } from '@mui/material';
2
+ import { translate } from '@arcblock/ux/lib/Locale/util';
3
+ import { useMemoizedFn } from 'ahooks';
4
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
+ import { UserMetadata } from '../../../@types';
6
+ import StatusDurationMenu from './duration-menu';
7
+ import StatusMenuItem, { BaseStatusProps, StatusItem, StyledMenu } from './menu-item';
8
+ import { translations } from '../../libs/locales';
9
+
10
+ interface StatusSelectorProps extends BaseStatusProps {
11
+ data: Array<StatusItem>;
12
+ open: boolean;
13
+ anchorEl?: PopoverProps['anchorEl'];
14
+ }
15
+
16
+ function StatusSelector({ data, open, onSelect, anchorEl, selected }: StatusSelectorProps) {
17
+ const { locale } = useLocaleContext();
18
+ const t = useMemoizedFn((key, _data = {}) => {
19
+ return translate(translations, key, locale, 'en', _data);
20
+ });
21
+ const onSelectStatus = (v?: UserMetadata['status']) => {
22
+ onSelect(v);
23
+ };
24
+
25
+ const renderMenuItems = () => {
26
+ return data.map((item) => {
27
+ if (item.children) {
28
+ return <StatusDurationMenu key={item.id} data={item} selected={selected} onSelect={onSelectStatus} />;
29
+ }
30
+ return (
31
+ <StatusMenuItem
32
+ key={item.id}
33
+ icon={item.icon}
34
+ selected={selected?.value === item.id}
35
+ onClick={() => onSelectStatus({ value: item.id })}>
36
+ {item.name}
37
+ </StatusMenuItem>
38
+ );
39
+ });
40
+ };
41
+
42
+ return (
43
+ <StyledMenu open={open} onClose={() => onSelectStatus()} anchorEl={anchorEl}>
44
+ <Typography component="span" color="text.secondary" pl={2} fontSize="14px">
45
+ {t('profile.setStatus')}
46
+ </Typography>
47
+ {renderMenuItems()}
48
+ </StyledMenu>
49
+ );
50
+ }
51
+
52
+ export default StatusSelector;
@@ -0,0 +1,52 @@
1
+ import { ListItemIcon, MenuItem, Menu } from '@mui/material';
2
+ import { SvgIconProps } from '@mui/material/SvgIcon';
3
+ import React from 'react';
4
+ import styled from '@emotion/styled';
5
+ import { temp as colors } from '@arcblock/ux/lib/Colors';
6
+ import { UserMetadata } from '../../../@types';
7
+
8
+ interface StatusMenuItemProps {
9
+ icon?: React.FC<SvgIconProps>;
10
+ selected?: boolean;
11
+ onClick: (e: React.MouseEvent<HTMLElement>) => void;
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ export default function StatusMenuItem({ icon, selected, onClick, children }: StatusMenuItemProps) {
16
+ return (
17
+ <MenuItem onClick={onClick} className={selected ? 'selected' : ''}>
18
+ {icon && (
19
+ <ListItemIcon style={{ minWidth: '24px' }}>
20
+ {React.createElement(icon, {
21
+ style: { fontSize: '16px', width: '16px', height: '16px' },
22
+ })}
23
+ </ListItemIcon>
24
+ )}
25
+ {children}
26
+ </MenuItem>
27
+ );
28
+ }
29
+
30
+ export const StyledMenu = styled(Menu)`
31
+ .MuiPaper-root {
32
+ box-shadow: 0px 6px 24px rgba(0, 0, 0, 0.15);
33
+ }
34
+ .MuiList-root {
35
+ min-width: 160px;
36
+ }
37
+ .selected {
38
+ background-color: ${colors.backgroundsBgSubtitle};
39
+ }
40
+ `;
41
+
42
+ export interface StatusItem {
43
+ id: string;
44
+ name: string;
45
+ icon?: React.FC<SvgIconProps>;
46
+ children?: Array<StatusItem>;
47
+ }
48
+
49
+ export interface BaseStatusProps {
50
+ selected: UserMetadata['status'];
51
+ onSelect: (v: UserMetadata['status']) => void;
52
+ }
@@ -24,7 +24,7 @@ import { PROFILE_URL } from '@arcblock/ux/lib/Util/constant';
24
24
  import Footer from '../../Footer';
25
25
  import Header from '../../Header';
26
26
  import { translations } from '../libs/locales';
27
- import { UserInfo, UserBasicInfo, SwitchRole } from './user-info';
27
+ import { UserBasicInfo } from './user-info';
28
28
  import type { SessionContext as TSessionContext, User, UserCenterTab } from '../../@types';
29
29
  // @ts-ignore
30
30
  import { formatBlockletInfo, getLink, getLocalizedNavigation } from '../../blocklets';
@@ -34,17 +34,18 @@ import { client } from '../../libs/client';
34
34
  import useMobile from '../../hooks/use-mobile';
35
35
  import { ConfigUserSpaceProvider } from '../../contexts/config-user-space';
36
36
  import DidSpace from './storage';
37
+ import Nft from './nft';
37
38
 
38
- const profileLink = joinURL(PROFILE_URL, '/profile');
39
+ const nftsLink = joinURL(PROFILE_URL, '/nfts');
39
40
  const settingsLink = joinURL(PROFILE_URL, '/settings');
40
41
  const didSpacesLink = joinURL(PROFILE_URL, '/did-spaces');
41
42
 
42
43
  const Main = styled(Box)(({ theme }) => ({
43
44
  flex: 1,
44
45
  boxSizing: 'border-box',
45
- padding: theme.spacing(3),
46
+ padding: '0 16px',
46
47
  width: '100%',
47
- maxWidth: 1200,
48
+ maxWidth: 1600,
48
49
  margin: '0 auto',
49
50
  display: 'flex',
50
51
  alignItems: 'stretch',
@@ -184,29 +185,31 @@ export default function UserCenter({
184
185
  }, []);
185
186
 
186
187
  const defaultTabs = useCreation((): any => {
187
- const tabs: any = [
188
- {
189
- label: t('common.profile'),
190
- protected: false,
191
- value: profileLink,
192
- url: getLink(profileLink, locale),
193
- },
194
- ];
188
+ let tabs: any = [];
195
189
  if (isMyself) {
196
- tabs.push({
197
- label: t('common.setting'),
198
- protected: true,
199
- isPrivate: true,
200
- value: settingsLink,
201
- url: getLink(settingsLink, locale),
202
- });
203
- tabs.push({
204
- label: t('storageManagement'),
205
- protected: true,
206
- isPrivate: true,
207
- value: didSpacesLink,
208
- url: getLink(didSpacesLink, locale),
209
- });
190
+ tabs = [
191
+ {
192
+ label: t('common.nft'),
193
+ protected: true,
194
+ isPrivate: true, // 隐私数据,仅自己可见
195
+ value: nftsLink,
196
+ url: getLink(nftsLink, locale),
197
+ },
198
+ {
199
+ label: t('common.setting'),
200
+ protected: true,
201
+ isPrivate: true,
202
+ value: settingsLink,
203
+ url: getLink(settingsLink, locale),
204
+ },
205
+ {
206
+ label: t('storageManagement'),
207
+ protected: true,
208
+ isPrivate: true,
209
+ value: didSpacesLink,
210
+ url: getLink(didSpacesLink, locale),
211
+ },
212
+ ];
210
213
  }
211
214
  return tabs;
212
215
  }, [isMyself, locale]);
@@ -224,7 +227,7 @@ export default function UserCenter({
224
227
  label: x.title || x.label,
225
228
  url: x.link || x.url,
226
229
  protected: privacyState?.data?.[value] ?? false,
227
- isPrivate: x.isPrivate,
230
+ isPrivate: x.isPrivate || x._rawLink === '/payment-kit/customer', // FIXME: HACK: 隐藏 payment-kit/customer 菜单, 需要一个通用的解决方案,在嵌入的时候就决定是否是私有的
228
231
  // icon: x.icon,
229
232
  };
230
233
  })
@@ -268,8 +271,11 @@ export default function UserCenter({
268
271
  return currentActiveTab && currentActiveTab?.value === joinURL(PROFILE_URL, '/settings');
269
272
  }, [currentActiveTab]);
270
273
 
271
- const isProfileTab = useCreation(() => {
272
- return currentActiveTab && currentActiveTab?.value === joinURL(PROFILE_URL, '/profile');
274
+ const isNftsTab = useCreation(() => {
275
+ return (
276
+ (currentActiveTab && currentActiveTab?.value === joinURL(PROFILE_URL, '/profile')) ||
277
+ currentActiveTab?.value === joinURL(PROFILE_URL, '/nfts')
278
+ );
273
279
  }, [currentActiveTab]);
274
280
 
275
281
  const isDidSpaceTab = useCreation(() => {
@@ -289,44 +295,26 @@ export default function UserCenter({
289
295
  });
290
296
 
291
297
  const renderDefaultTab = useCreation(() => {
292
- if (isProfileTab) {
298
+ if (isNftsTab && isMyself) {
293
299
  return (
294
300
  <Box
295
301
  sx={{
296
- maxWidth: '100%',
297
- position: 'relative',
302
+ display: 'flex',
303
+ flexDirection: 'column',
304
+ gap: 2.5,
298
305
  }}>
299
- <Box
300
- sx={{
301
- display: 'flex',
302
- flexDirection: 'column',
303
- gap: 2.5,
304
- position: {
305
- xs: 'static',
306
- md: stickySidebar ? 'sticky' : 'static',
307
- },
308
- top: (theme) => (stickySidebar ? theme.spacing(3) : 'unset'),
309
- }}>
310
- <Box
306
+ <Box sx={{ border: `1px solid ${colors.dividerColor}`, borderRadius: 2, p: 2 }}>
307
+ <Typography
311
308
  sx={{
312
- width: {
313
- sx: '100%',
314
- md: 420,
315
- },
309
+ color: colors.foregroundsFgBase,
310
+ fontWeight: 600,
311
+ mb: 2.5,
316
312
  }}>
317
- <Box display="flex" justifyContent="space-between">
318
- <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{isMyself ? t('myInfo') : t('hisInfo')}</Typography>
319
- {isMyself ? <SwitchRole user={userState.data as User} switchPassport={handleSwitchPassport} /> : null}
320
- </Box>
321
- <UserInfo user={userState.data as User} isMySelf={isMyself} />
322
- </Box>
323
- {isMyself ? (
324
- <Box>
325
- <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{t('passport')}</Typography>
326
- <Passport user={userState.data as User} />
327
- </Box>
328
- ) : null}
313
+ {t('passport')}
314
+ </Typography>
315
+ <Passport user={userState.data as User} />
329
316
  </Box>
317
+ <Nft user={userState.data as User} />
330
318
  </Box>
331
319
  );
332
320
  }
@@ -341,7 +329,7 @@ export default function UserCenter({
341
329
  );
342
330
  }
343
331
  return null;
344
- }, [isSettingTab, isProfileTab, userState, isMyself, stickySidebar, settingContent]);
332
+ }, [isSettingTab, isNftsTab, userState, isMyself, stickySidebar, settingContent]);
345
333
 
346
334
  const emptyContent = useCreation(() => {
347
335
  return (
@@ -464,57 +452,70 @@ export default function UserCenter({
464
452
  }
465
453
 
466
454
  return (
467
- <ContentWrapper>
455
+ <ContentWrapper display="flex" flexDirection={isMobile ? 'column' : 'row'}>
456
+ <Box flex="1" order={isMobile ? 2 : 'unset'}>
457
+ {userCenterTabs.length > 0 && currentTab ? (
458
+ <Box
459
+ display="flex"
460
+ flexDirection="column"
461
+ sx={{
462
+ height: '100%',
463
+ overflow: 'auto',
464
+ padding: '1px',
465
+ }}>
466
+ <Tabs
467
+ orientation="horizontal"
468
+ variant="line"
469
+ tabs={userCenterTabs}
470
+ current={currentActiveTab?.value ?? currentTab}
471
+ onChange={handleChangeTab}
472
+ sx={{
473
+ mb: 2,
474
+ '.MuiTabs-flexContainer': {
475
+ gap: 3,
476
+ '.MuiButtonBase-root': {
477
+ padding: '40px 4px 32px 4px',
478
+ fontSize: 16,
479
+ },
480
+ '.MuiTab-root': {
481
+ display: 'block',
482
+ textAlign: 'left',
483
+ alignItems: 'flex-start',
484
+ justifyContent: 'flex-start',
485
+ fontWeight: 400,
486
+ },
487
+ },
488
+ '.MuiTabs-scroller': {
489
+ '&:after': {
490
+ content: '""',
491
+ display: 'block',
492
+ width: '100%',
493
+ height: '1px',
494
+ backgroundColor: `${colors.dividerColor} !important`,
495
+ },
496
+ },
497
+ }}
498
+ />
499
+ {tabContent}
500
+ </Box>
501
+ ) : null}
502
+ {userCenterTabs.length === 0 && emptyContent}
503
+ </Box>
504
+ {!isMobile && <Divider orientation="vertical" sx={{ borderColor: colors.dividerColor, ml: 5 }} />}
468
505
  <UserBasicInfo
506
+ isMobile={isMobile}
507
+ order={isMobile ? 1 : 'unset'}
469
508
  isMyself={isMyself}
470
509
  switchPassport={handleSwitchPassport}
471
510
  switchProfile={session.switchProfile}
472
511
  user={userState.data as User}
473
512
  showFullDid={false}
474
513
  sx={{
475
- display: 'flex',
476
- gap: 3,
477
- mt: 2,
514
+ padding: !isMobile ? '40px 24px 24px 40px' : '24px 0',
515
+ ...(!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {}),
516
+ boxSizing: 'content-box',
478
517
  }}
479
518
  />
480
- <Divider sx={{ mt: 3, mb: 3, borderColor: colors.dividerColor }} />
481
- {userCenterTabs.length > 0 && currentTab ? (
482
- <Box
483
- display={isMobile ? 'block' : 'flex'}
484
- sx={{
485
- height: '100%',
486
- overflow: 'auto',
487
- padding: '1px',
488
- }}>
489
- <Tabs
490
- orientation={isMobile ? 'horizontal' : 'vertical'}
491
- variant="line"
492
- tabs={userCenterTabs}
493
- current={currentActiveTab?.value ?? currentTab}
494
- onChange={handleChangeTab}
495
- sx={{
496
- width: isMobile ? '100%' : 160,
497
- marginBottom: isMobile ? '8px' : 0,
498
- flexShrink: 0,
499
- '.MuiTabs-flexContainer': {
500
- gap: 1.5,
501
- '.MuiButtonBase-root': {
502
- fontSize: 16,
503
- },
504
- '.MuiTab-root': {
505
- display: 'block',
506
- textAlign: 'left',
507
- alignItems: 'flex-start',
508
- justifyContent: 'flex-start',
509
- },
510
- },
511
- }}
512
- />
513
- <Divider orientation="vertical" sx={{ height: '100%', mr: 3, borderColor: colors.dividerColor }} />
514
- {tabContent}
515
- </Box>
516
- ) : null}
517
- {userCenterTabs.length === 0 && emptyContent}
518
519
  </ContentWrapper>
519
520
  );
520
521
  }, [
@@ -0,0 +1,29 @@
1
+ import { useState, useEffect } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import utc from 'dayjs/plugin/utc';
4
+ import timezonePlugin from 'dayjs/plugin/timezone';
5
+ import { formatToDatetime } from '@arcblock/ux/lib/Util';
6
+ import { useCreation } from 'ahooks';
7
+
8
+ dayjs.extend(utc);
9
+ dayjs.extend(timezonePlugin);
10
+
11
+ export default function Clock({ timezone = 'utc', locale = 'zh' }: { timezone?: string; locale?: string }) {
12
+ const [time, setTime] = useState(dayjs().tz(timezone));
13
+
14
+ useEffect(() => {
15
+ const timerId = setInterval(() => {
16
+ setTime(dayjs().tz(timezone));
17
+ }, 6000); // 每分钟更新一次
18
+
19
+ // 清除定时器
20
+ return () => clearInterval(timerId);
21
+ }, [timezone]);
22
+
23
+ const formatTime = useCreation(() => {
24
+ const localeOption = locale === 'zh' ? 'zh-cn' : 'en-us';
25
+ return formatToDatetime(time.toDate(), { tz: timezone, locale: localeOption });
26
+ }, [time, timezone, locale]);
27
+
28
+ return <div>{formatTime}</div>;
29
+ }