@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.
- package/lib/@types/index.d.ts +34 -0
- package/lib/@types/index.js +16 -0
- package/lib/@types/shims.d.ts +1 -0
- package/lib/UserCenter/components/config-profile.js +23 -1
- package/lib/UserCenter/components/editable-field.d.ts +22 -0
- package/lib/UserCenter/components/editable-field.js +159 -0
- package/lib/UserCenter/components/nft.d.ts +4 -0
- package/lib/UserCenter/components/nft.js +93 -0
- package/lib/UserCenter/components/settings.js +32 -15
- package/lib/UserCenter/components/status-selector/duration-menu.d.ts +9 -0
- package/lib/UserCenter/components/status-selector/duration-menu.js +75 -0
- package/lib/UserCenter/components/status-selector/index.d.ts +9 -0
- package/lib/UserCenter/components/status-selector/index.js +39 -0
- package/lib/UserCenter/components/status-selector/menu-item.d.ts +24 -0
- package/lib/UserCenter/components/status-selector/menu-item.js +24 -0
- package/lib/UserCenter/components/user-center.js +119 -122
- package/lib/UserCenter/components/user-info/clock.d.ts +4 -0
- package/lib/UserCenter/components/user-info/clock.js +23 -0
- package/lib/UserCenter/components/user-info/link-preview-input.d.ts +5 -0
- package/lib/UserCenter/components/user-info/link-preview-input.js +181 -0
- package/lib/UserCenter/components/user-info/metadata.d.ts +7 -0
- package/lib/UserCenter/components/user-info/metadata.js +458 -0
- package/lib/UserCenter/components/user-info/switch-role.js +2 -3
- package/lib/UserCenter/components/user-info/user-basic-info.d.ts +2 -0
- package/lib/UserCenter/components/user-info/user-basic-info.js +159 -90
- package/lib/UserCenter/components/user-info/user-info.js +2 -16
- package/lib/UserCenter/components/user-info/user-status.d.ts +8 -0
- package/lib/UserCenter/components/user-info/user-status.js +153 -0
- package/lib/UserCenter/components/user-info/utils.d.ts +19 -0
- package/lib/UserCenter/components/user-info/utils.js +86 -0
- package/lib/UserCenter/libs/locales.d.ts +65 -0
- package/lib/UserCenter/libs/locales.js +67 -2
- package/lib/UserSessions/components/user-sessions.js +48 -14
- package/package.json +8 -5
- package/src/@types/index.ts +39 -0
- package/src/@types/shims.d.ts +1 -0
- package/src/UserCenter/components/config-profile.tsx +20 -1
- package/src/UserCenter/components/editable-field.tsx +180 -0
- package/src/UserCenter/components/nft.tsx +122 -0
- package/src/UserCenter/components/settings.tsx +16 -4
- package/src/UserCenter/components/status-selector/duration-menu.tsx +87 -0
- package/src/UserCenter/components/status-selector/index.tsx +52 -0
- package/src/UserCenter/components/status-selector/menu-item.tsx +52 -0
- package/src/UserCenter/components/user-center.tsx +104 -103
- package/src/UserCenter/components/user-info/clock.tsx +29 -0
- package/src/UserCenter/components/user-info/link-preview-input.tsx +227 -0
- package/src/UserCenter/components/user-info/metadata.tsx +465 -0
- package/src/UserCenter/components/user-info/switch-role.tsx +3 -3
- package/src/UserCenter/components/user-info/user-basic-info.tsx +150 -87
- package/src/UserCenter/components/user-info/user-info.tsx +6 -16
- package/src/UserCenter/components/user-info/user-status.tsx +182 -0
- package/src/UserCenter/components/user-info/utils.ts +114 -0
- package/src/UserCenter/libs/locales.ts +65 -0
- package/src/UserSessions/components/user-sessions.tsx +68 -18
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import { Box,
|
|
1
|
+
import { Box, Divider, Typography } from '@mui/material';
|
|
2
2
|
import type { BoxProps } from '@mui/material';
|
|
3
3
|
import Avatar from '@arcblock/ux/lib/Avatar';
|
|
4
|
-
import { useTheme } from '@arcblock/ux/lib/Theme';
|
|
5
4
|
import DID from '@arcblock/ux/lib/DID';
|
|
6
5
|
import { useMemoizedFn } from 'ahooks';
|
|
7
6
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
8
7
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
9
8
|
import noop from 'lodash/noop';
|
|
10
|
-
import
|
|
11
|
-
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
12
|
-
import { useContext, useState } from 'react';
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
13
10
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
11
|
+
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
14
12
|
import type { AxiosError } from 'axios';
|
|
15
|
-
import
|
|
16
|
-
import { SessionContext as TSessionContext } from '../../../@types';
|
|
13
|
+
import { parseURL, joinURL } from 'ufo';
|
|
17
14
|
|
|
18
15
|
import { translations } from '../../libs/locales';
|
|
19
|
-
import type { User } from '../../../@types';
|
|
16
|
+
import type { User, UserMetadata } from '../../../@types';
|
|
20
17
|
import { formatAxiosError } from '../../libs/utils';
|
|
18
|
+
import { getStatusDuration, isValidUrl } from './utils';
|
|
21
19
|
import SwitchRole from './switch-role';
|
|
20
|
+
import UserMetadataComponent from './metadata';
|
|
21
|
+
import UserStatus from './user-status';
|
|
22
|
+
import UserInfo from './user-info';
|
|
23
|
+
import { client } from '../../../libs/client';
|
|
22
24
|
|
|
23
25
|
export default function UserBasicInfo({
|
|
24
26
|
user,
|
|
@@ -33,34 +35,76 @@ export default function UserBasicInfo({
|
|
|
33
35
|
showFullDid?: boolean;
|
|
34
36
|
switchPassport: () => void;
|
|
35
37
|
switchProfile: () => void;
|
|
38
|
+
size?: number;
|
|
39
|
+
isMobile?: boolean;
|
|
36
40
|
} & BoxProps) {
|
|
37
41
|
const { locale } = useLocaleContext();
|
|
38
|
-
const
|
|
39
|
-
const [loading, setLoading] = useState(false);
|
|
40
|
-
|
|
42
|
+
const [userStatus, setUserStatus] = useState<UserMetadata['status']>(undefined);
|
|
41
43
|
const t = useMemoizedFn((key, data = {}) => {
|
|
42
44
|
return translate(translations, key, locale, 'en', data);
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setUserStatus(user?.metadata?.status);
|
|
49
|
+
}, [user]);
|
|
46
50
|
|
|
47
|
-
const
|
|
51
|
+
const onUpdateUserStatus = async (v: UserMetadata['status']) => {
|
|
52
|
+
if (!isMyself) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
if (v) {
|
|
57
|
+
const dateRange = getStatusDuration(v);
|
|
58
|
+
v.dateRange = dateRange.length > 0 ? dateRange : (userStatus?.dateRange ?? []);
|
|
59
|
+
}
|
|
60
|
+
setUserStatus(v);
|
|
61
|
+
await client.user.saveProfile({
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
metadata: {
|
|
64
|
+
...(user?.metadata ?? { joinedAt: user?.createdAt, email: user?.email, phone: user?.phone }),
|
|
65
|
+
status: v,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(err);
|
|
70
|
+
Toast.error(formatAxiosError(err as AxiosError));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
48
73
|
|
|
49
74
|
if (!user) {
|
|
50
75
|
return null;
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
const
|
|
78
|
+
const onSave = async (v: UserMetadata) => {
|
|
79
|
+
if (!isMyself) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
54
82
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
const newLinks =
|
|
84
|
+
v?.links
|
|
85
|
+
?.map((link) => {
|
|
86
|
+
if (!link.url || !isValidUrl(link.url)) return null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const parsedUrl = parseURL(link.url);
|
|
90
|
+
// 如果没有协议,添加 https
|
|
91
|
+
if (!parsedUrl.protocol) {
|
|
92
|
+
link.url = joinURL('https://', link.url);
|
|
93
|
+
}
|
|
94
|
+
return link;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error('Invalid URL:', link.url);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.filter((l) => !!l) || [];
|
|
101
|
+
v.links = newLinks;
|
|
102
|
+
// TODO: 需要更新 SDK
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
await client.user.saveProfile({ metadata: v });
|
|
59
105
|
} catch (err) {
|
|
60
106
|
console.error(err);
|
|
61
107
|
Toast.error(formatAxiosError(err as AxiosError));
|
|
62
|
-
} finally {
|
|
63
|
-
setLoading(false);
|
|
64
108
|
}
|
|
65
109
|
};
|
|
66
110
|
|
|
@@ -69,77 +113,96 @@ export default function UserBasicInfo({
|
|
|
69
113
|
{...rest}
|
|
70
114
|
sx={{
|
|
71
115
|
position: 'relative',
|
|
72
|
-
...rest.sx,
|
|
116
|
+
...(rest.sx ?? {}),
|
|
73
117
|
}}>
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
<Box className="user-info" display="flex" flexDirection={rest.isMobile ? 'row' : 'column'} gap={2}>
|
|
119
|
+
<Box
|
|
120
|
+
className="user-avatar"
|
|
121
|
+
position="relative"
|
|
122
|
+
display="flex"
|
|
123
|
+
alignItems="center"
|
|
124
|
+
justifyContent="space-between">
|
|
125
|
+
<Avatar
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
src={user?.avatar}
|
|
128
|
+
did={user?.did}
|
|
129
|
+
size={rest.size || (rest.isMobile ? 64 : 100)}
|
|
130
|
+
variant="circle"
|
|
131
|
+
shape="circle"
|
|
132
|
+
sx={{
|
|
133
|
+
borderRadius: '100%',
|
|
134
|
+
backgroundColor: '#fff',
|
|
135
|
+
position: 'relative',
|
|
136
|
+
overflow: 'hidden',
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
...(isMyself
|
|
139
|
+
? {
|
|
140
|
+
cursor: 'pointer',
|
|
141
|
+
'&::after': {
|
|
142
|
+
content: `"${t('switchProfile')}"`,
|
|
143
|
+
color: 'white',
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
fontSize: '12px',
|
|
146
|
+
bottom: 0,
|
|
147
|
+
left: 0,
|
|
148
|
+
right: 0,
|
|
149
|
+
height: '50%',
|
|
150
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
151
|
+
display: 'flex',
|
|
152
|
+
justifyContent: 'center',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
157
|
+
}}
|
|
158
|
+
onClick={isMyself ? switchProfile : noop}
|
|
159
|
+
/>
|
|
160
|
+
<UserStatus
|
|
161
|
+
isMobile={rest.isMobile}
|
|
162
|
+
size={rest.size || (rest.isMobile ? 64 : 100)}
|
|
163
|
+
isMyself={isMyself}
|
|
164
|
+
status={userStatus}
|
|
165
|
+
onChange={onUpdateUserStatus}
|
|
166
|
+
/>
|
|
167
|
+
</Box>
|
|
168
|
+
<Box
|
|
116
169
|
sx={{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
alignItems: 'center',
|
|
120
|
-
gap: 1,
|
|
121
|
-
fontSize: '24px !important',
|
|
170
|
+
flex: 1,
|
|
171
|
+
overflow: 'hidden',
|
|
122
172
|
}}>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
173
|
+
<Typography
|
|
174
|
+
variant="h6"
|
|
175
|
+
sx={{
|
|
176
|
+
fontWeight: 600,
|
|
177
|
+
display: 'flex',
|
|
178
|
+
alignItems: 'center',
|
|
179
|
+
gap: 1,
|
|
180
|
+
fontSize: '24px !important',
|
|
181
|
+
}}>
|
|
182
|
+
{user?.fullName}
|
|
183
|
+
{isMyself ? <SwitchRole user={user} switchPassport={switchPassport} /> : null}
|
|
184
|
+
</Typography>
|
|
185
|
+
<DID
|
|
186
|
+
did={user.did}
|
|
187
|
+
showQrcode
|
|
188
|
+
copyable
|
|
189
|
+
compact={!showFullDid}
|
|
190
|
+
responsive={!showFullDid}
|
|
191
|
+
locale={locale}
|
|
192
|
+
style={{ maxWidth: 260 }}
|
|
193
|
+
/>
|
|
194
|
+
</Box>
|
|
142
195
|
</Box>
|
|
196
|
+
<UserMetadataComponent isMobile={rest.isMobile} isMyself={isMyself} user={user} onSave={onSave} />
|
|
197
|
+
{isMyself ? (
|
|
198
|
+
<>
|
|
199
|
+
<Divider sx={{ my: 3, borderColor: colors.dividerColor }} />
|
|
200
|
+
<Typography component="p" color="text.secondary" fontSize="14px" mb={2}>
|
|
201
|
+
{t('profile.justForYou')}
|
|
202
|
+
</Typography>
|
|
203
|
+
<UserInfo user={user} isMySelf={isMyself} />
|
|
204
|
+
</>
|
|
205
|
+
) : null}
|
|
143
206
|
</Box>
|
|
144
207
|
);
|
|
145
208
|
}
|
|
@@ -3,8 +3,6 @@ import type { BoxProps } from '@mui/material';
|
|
|
3
3
|
import { Icon } from '@iconify/react';
|
|
4
4
|
import { useMemoizedFn, useCreation } from 'ahooks';
|
|
5
5
|
import DID from '@arcblock/ux/lib/DID';
|
|
6
|
-
import MailOutlineRoundedIcon from '@iconify-icons/material-symbols/mail-outline-rounded';
|
|
7
|
-
import PhoneOutlineRoundedIcon from '@iconify-icons/material-symbols/phone-android-outline-rounded';
|
|
8
6
|
import ScheduleOutlineRoundedIcon from '@iconify-icons/material-symbols/schedule-outline-rounded';
|
|
9
7
|
import MoreTimeRoundedIcon from '@iconify-icons/material-symbols/more-time-rounded';
|
|
10
8
|
import CaptivePortalRoundedIcon from '@iconify-icons/material-symbols/captive-portal-rounded';
|
|
@@ -37,18 +35,6 @@ export default function UserInfo({
|
|
|
37
35
|
|
|
38
36
|
const userInfoListData = [];
|
|
39
37
|
if (isMySelf) {
|
|
40
|
-
userInfoListData.push({
|
|
41
|
-
icon: <Icon fontSize={16} icon={MailOutlineRoundedIcon} />,
|
|
42
|
-
title: t('email'),
|
|
43
|
-
content: user?.email || t('emptyField'),
|
|
44
|
-
verified: user?.emailVerified,
|
|
45
|
-
});
|
|
46
|
-
userInfoListData.push({
|
|
47
|
-
icon: <Icon fontSize={16} icon={PhoneOutlineRoundedIcon} />,
|
|
48
|
-
title: t('phone'),
|
|
49
|
-
content: user?.phone || t('emptyField'),
|
|
50
|
-
verified: user?.phoneVerified,
|
|
51
|
-
});
|
|
52
38
|
userInfoListData.push({
|
|
53
39
|
icon: <Icon fontSize={16} icon={ScheduleOutlineRoundedIcon} />,
|
|
54
40
|
title: t('lastLoginAt'),
|
|
@@ -80,7 +66,11 @@ export default function UserInfo({
|
|
|
80
66
|
userInfoListData.push({
|
|
81
67
|
icon: <Icon fontSize={16} icon={CaptivePortalRoundedIcon} />,
|
|
82
68
|
title: t('invitedBy'),
|
|
83
|
-
content: user?.inviter ?
|
|
69
|
+
content: user?.inviter ? (
|
|
70
|
+
<DID style={{ maxWidth: 260 }} did={user.inviter} showQrcode copyable compact responsive locale={locale} />
|
|
71
|
+
) : (
|
|
72
|
+
'-'
|
|
73
|
+
),
|
|
84
74
|
});
|
|
85
75
|
}
|
|
86
76
|
|
|
@@ -94,7 +84,7 @@ export default function UserInfo({
|
|
|
94
84
|
...rest?.sx,
|
|
95
85
|
}}>
|
|
96
86
|
{userInfoListData.map((item) => (
|
|
97
|
-
<UserInfoItem key={item.title} data={item} sx={{ flex: 1 }}
|
|
87
|
+
<UserInfoItem key={item.title} data={item} sx={{ flex: 1 }} />
|
|
98
88
|
))}
|
|
99
89
|
</Box>
|
|
100
90
|
);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import Badge from '@mui/material/Badge';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import styled from '@emotion/styled';
|
|
5
|
+
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { useCreation, useMemoizedFn, useInterval, useUnmount } from 'ahooks';
|
|
7
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
9
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
10
|
+
import { SvgIconProps } from '@mui/material/SvgIcon';
|
|
11
|
+
import { formatToDatetime } from '@arcblock/ux/lib/Util';
|
|
12
|
+
import type { UserMetadata } from '../../../@types';
|
|
13
|
+
import { DurationEnum, StatusEnum } from '../../../@types';
|
|
14
|
+
import StatusSelector from '../status-selector';
|
|
15
|
+
import { translations } from '../../libs/locales';
|
|
16
|
+
import { getTimeRemaining, isWithinTimeRange } from './utils';
|
|
17
|
+
|
|
18
|
+
const MeetingIcon = lazy(() => import('@arcblock/icons/lib/Meeting'));
|
|
19
|
+
const CommunityIcon = lazy(() => import('@arcblock/icons/lib/Community'));
|
|
20
|
+
const HolidayIcon = lazy(() => import('@arcblock/icons/lib/Holiday'));
|
|
21
|
+
const OffSickIcon = lazy(() => import('@arcblock/icons/lib/OffSick'));
|
|
22
|
+
const WorkingRemotelyIcon = lazy(() => import('@arcblock/icons/lib/WorkingRemotely'));
|
|
23
|
+
|
|
24
|
+
const StatusIconMap: Record<StatusEnum, React.FC<SvgIconProps> | undefined> = {
|
|
25
|
+
[StatusEnum.Meeting]: MeetingIcon,
|
|
26
|
+
[StatusEnum.Community]: CommunityIcon,
|
|
27
|
+
[StatusEnum.Holiday]: HolidayIcon,
|
|
28
|
+
[StatusEnum.OffSick]: OffSickIcon,
|
|
29
|
+
[StatusEnum.WorkingRemotely]: WorkingRemotelyIcon,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default function UserStatus({
|
|
33
|
+
isMobile,
|
|
34
|
+
size,
|
|
35
|
+
isMyself,
|
|
36
|
+
status,
|
|
37
|
+
onChange,
|
|
38
|
+
}: {
|
|
39
|
+
isMobile?: boolean;
|
|
40
|
+
size: number;
|
|
41
|
+
isMyself: boolean;
|
|
42
|
+
status: UserMetadata['status'];
|
|
43
|
+
onChange: (v: UserMetadata['status']) => void;
|
|
44
|
+
}) {
|
|
45
|
+
const { locale } = useLocaleContext();
|
|
46
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
47
|
+
return translate(translations, key, locale, 'en', data);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [interval, setInterval] = useState<number | undefined>(undefined);
|
|
51
|
+
|
|
52
|
+
const pauseInterval = useMemoizedFn(() => {
|
|
53
|
+
setInterval(undefined);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setInterval(1000);
|
|
58
|
+
}, [status]);
|
|
59
|
+
|
|
60
|
+
const clear = useInterval(() => {
|
|
61
|
+
if (status?.value && status?.dateRange?.length === 2) {
|
|
62
|
+
const isWithin = isWithinTimeRange(status.dateRange as [Date, Date]);
|
|
63
|
+
if (!isWithin) {
|
|
64
|
+
pauseInterval();
|
|
65
|
+
onChange(undefined);
|
|
66
|
+
} else {
|
|
67
|
+
// 根据距离结束时间长度,设置 interval
|
|
68
|
+
const timeRemaining = getTimeRemaining(status.dateRange[1]);
|
|
69
|
+
if (timeRemaining > 0) {
|
|
70
|
+
setInterval(timeRemaining);
|
|
71
|
+
} else {
|
|
72
|
+
pauseInterval();
|
|
73
|
+
onChange(undefined);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
pauseInterval();
|
|
78
|
+
}
|
|
79
|
+
}, interval);
|
|
80
|
+
|
|
81
|
+
useUnmount(() => {
|
|
82
|
+
clear();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
|
86
|
+
|
|
87
|
+
const getDurationData = useCallback(() => {
|
|
88
|
+
const data = Object.keys(DurationEnum).map((key) => ({
|
|
89
|
+
id: DurationEnum[key as keyof typeof DurationEnum],
|
|
90
|
+
name: t(`userStatus.duration.${key}`),
|
|
91
|
+
}));
|
|
92
|
+
return data;
|
|
93
|
+
}, [t]);
|
|
94
|
+
|
|
95
|
+
const statusData = useCreation(() => {
|
|
96
|
+
const durationData = getDurationData();
|
|
97
|
+
return Object.keys(StatusEnum).map((key) => ({
|
|
98
|
+
id: StatusEnum[key as keyof typeof StatusEnum],
|
|
99
|
+
name: t(`userStatus.${key}`),
|
|
100
|
+
icon: StatusIconMap[StatusEnum[key as keyof typeof StatusEnum]],
|
|
101
|
+
children: durationData,
|
|
102
|
+
}));
|
|
103
|
+
}, [t, getDurationData]);
|
|
104
|
+
|
|
105
|
+
const onOpenStatusSelector = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
106
|
+
if (!isMyself) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setAnchorEl(event.currentTarget);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const onCloseStatusSelector = () => {
|
|
113
|
+
setAnchorEl(null);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onStatusChange = (v?: UserMetadata['status']) => {
|
|
117
|
+
if (!isMyself) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (v) {
|
|
121
|
+
onChange(v);
|
|
122
|
+
}
|
|
123
|
+
onCloseStatusSelector();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const StatusIcon = StatusIconMap[status?.value as keyof typeof StatusIconMap];
|
|
127
|
+
|
|
128
|
+
const tooltipTitle = useMemo(() => {
|
|
129
|
+
const currentStatus = statusData.find((item) => item.id === status?.value);
|
|
130
|
+
if (currentStatus) {
|
|
131
|
+
const localeOption = locale === 'zh' ? 'zh-cn' : 'en-us';
|
|
132
|
+
const range = status?.dateRange?.map((item) => {
|
|
133
|
+
return formatToDatetime(item, { locale: localeOption });
|
|
134
|
+
});
|
|
135
|
+
return `${currentStatus?.name}: ${range?.join('~')}`;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}, [status, statusData, locale]);
|
|
139
|
+
|
|
140
|
+
const open = Boolean(anchorEl);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<StatusDiv size={size} isMobile={isMobile}>
|
|
144
|
+
<Tooltip title={tooltipTitle}>
|
|
145
|
+
<Box
|
|
146
|
+
className="status-icon"
|
|
147
|
+
display="flex"
|
|
148
|
+
alignItems="center"
|
|
149
|
+
justifyContent="center"
|
|
150
|
+
onClick={onOpenStatusSelector}>
|
|
151
|
+
{StatusIcon ? <StatusIcon style={{ width: 16, height: 16 }} /> : <Badge color="success" variant="dot" />}
|
|
152
|
+
</Box>
|
|
153
|
+
</Tooltip>
|
|
154
|
+
<StatusSelector selected={status} data={statusData} open={open} onSelect={onStatusChange} anchorEl={anchorEl} />
|
|
155
|
+
</StatusDiv>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const StatusDiv = styled(Box)<{ size: number; isMobile?: boolean }>`
|
|
160
|
+
position: absolute;
|
|
161
|
+
left: ${({ size }) => `${(size * 3) / 4}px`};
|
|
162
|
+
top: ${({ size }) => `${size * 0.65}px`};
|
|
163
|
+
width: ${({ isMobile }) => (isMobile ? '22px' : '32px')};
|
|
164
|
+
height: ${({ isMobile }) => (isMobile ? '22px' : '32px')};
|
|
165
|
+
border-radius: ${({ isMobile }) => (isMobile ? '11px' : '16px')};
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
background-color: #ffffff;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
|
|
174
|
+
.status-icon {
|
|
175
|
+
flex-shrink: 0;
|
|
176
|
+
width: ${({ isMobile }) => (isMobile ? '16px' : '26px')};
|
|
177
|
+
height: ${({ isMobile }) => (isMobile ? '16px' : '26px')};
|
|
178
|
+
border-radius: ${({ isMobile }) => (isMobile ? '8px' : '13px')};
|
|
179
|
+
background-color: #eff1f5;
|
|
180
|
+
// background-color: #39b380;
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import moment from 'moment-timezone';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { DurationEnum, UserMetadata } from '../../../@types';
|
|
4
|
+
|
|
5
|
+
const HOUR = 3600;
|
|
6
|
+
const MINUTES_30 = 1800;
|
|
7
|
+
const MINUTES_10 = 600;
|
|
8
|
+
const MINUTES_5 = 300;
|
|
9
|
+
const MINUTES_1 = 60;
|
|
10
|
+
const SECOND = 1;
|
|
11
|
+
export const getTimezones = () => {
|
|
12
|
+
const timezones = moment.tz.names();
|
|
13
|
+
|
|
14
|
+
const formattedTimezones = timezones.map((tz) => {
|
|
15
|
+
const offset = moment.tz(tz).utcOffset() / 60; // 计算 UTC 偏移 (小时)
|
|
16
|
+
const hours = Math.floor(offset);
|
|
17
|
+
const minutes = (offset % 1) * 60;
|
|
18
|
+
const label = `GMT${hours >= 0 ? '+' : ''}${hours}:${minutes === 30 ? '30' : '00'}`;
|
|
19
|
+
|
|
20
|
+
return { label, value: tz };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return formattedTimezones
|
|
24
|
+
.sort((a, b) => {
|
|
25
|
+
const [hoursA, minutesA] = a.label.replace('GMT', '').split(':').map(Number);
|
|
26
|
+
const [hoursB, minutesB] = b.label.replace('GMT', '').split(':').map(Number);
|
|
27
|
+
const totalOffsetA = hoursA * 60 + minutesA; // 统一为分钟数
|
|
28
|
+
const totalOffsetB = hoursB * 60 + minutesB;
|
|
29
|
+
return totalOffsetB - totalOffsetA; // **降序排列**
|
|
30
|
+
})
|
|
31
|
+
.map((tz) => ({
|
|
32
|
+
label: `(${tz.label}) ${tz.value}`,
|
|
33
|
+
value: tz.value,
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const isValidUrl = (url: string) => {
|
|
38
|
+
const urlPattern =
|
|
39
|
+
/^(https?:\/\/)?((([a-zA-Z\d]([a-zA-Z\d-]*[a-zA-Z\d])*)\.)+[a-zA-Z]{2,}|((\d{1,3}\.){3}\d{1,3}))(:\d+)?(\/[-a-zA-Z\d%_.~+]*)*(\?[;&a-zA-Z\d%_.~+=-]*)?(#[a-zA-Z\d_]*)?$/;
|
|
40
|
+
return urlPattern.test(url);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 根据 duration 类型,计算出date range
|
|
45
|
+
* @param status
|
|
46
|
+
* @returns
|
|
47
|
+
*/
|
|
48
|
+
export const getStatusDuration = (status: UserMetadata['status']) => {
|
|
49
|
+
let dateRange: dayjs.Dayjs[] = [];
|
|
50
|
+
const current = dayjs();
|
|
51
|
+
switch (status?.duration) {
|
|
52
|
+
case DurationEnum.ThirtyMinutes:
|
|
53
|
+
dateRange = [current, current.add(30, 'minutes')];
|
|
54
|
+
break;
|
|
55
|
+
case DurationEnum.OneHour:
|
|
56
|
+
dateRange = [current, current.add(1, 'hour')];
|
|
57
|
+
break;
|
|
58
|
+
case DurationEnum.FourHours:
|
|
59
|
+
dateRange = [current, current.add(4, 'hours')];
|
|
60
|
+
break;
|
|
61
|
+
case DurationEnum.Today:
|
|
62
|
+
dateRange = [current, current.endOf('day')];
|
|
63
|
+
break;
|
|
64
|
+
case DurationEnum.ThisWeek:
|
|
65
|
+
dateRange = [current, current.endOf('week')];
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
return dateRange.map((d) => d.toDate());
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 根据状态的 duration,判断是否在时间范围内
|
|
75
|
+
* @param status
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
export const isWithinTimeRange = (dateRange: [Date, Date]) => {
|
|
79
|
+
const current = dayjs();
|
|
80
|
+
return current.isAfter(dayjs(dateRange[0])) && current.isBefore(dayjs(dateRange[1]));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 获取当前时间距离结束时间还有多久
|
|
85
|
+
*/
|
|
86
|
+
export const getTimeRemaining = (date: Date) => {
|
|
87
|
+
const now = dayjs();
|
|
88
|
+
const end = dayjs(date);
|
|
89
|
+
const diffSeconds = end.diff(now, 'seconds');
|
|
90
|
+
|
|
91
|
+
// 转换为毫秒
|
|
92
|
+
const toMilliseconds = (seconds: number) => seconds * 1000;
|
|
93
|
+
|
|
94
|
+
if (diffSeconds >= HOUR) {
|
|
95
|
+
return toMilliseconds(HOUR); // 1小时 = 3600000ms
|
|
96
|
+
}
|
|
97
|
+
if (diffSeconds >= MINUTES_30) {
|
|
98
|
+
return toMilliseconds(MINUTES_30); // 30分钟 = 1800000ms
|
|
99
|
+
}
|
|
100
|
+
if (diffSeconds >= MINUTES_10) {
|
|
101
|
+
return toMilliseconds(MINUTES_10); // 10分钟 = 600000ms
|
|
102
|
+
}
|
|
103
|
+
if (diffSeconds >= MINUTES_5) {
|
|
104
|
+
return toMilliseconds(MINUTES_5); // 5分钟 = 300000ms
|
|
105
|
+
}
|
|
106
|
+
if (diffSeconds >= MINUTES_1) {
|
|
107
|
+
return toMilliseconds(MINUTES_1); // 1分钟 = 60000ms
|
|
108
|
+
}
|
|
109
|
+
if (diffSeconds >= SECOND) {
|
|
110
|
+
return toMilliseconds(SECOND); // 1秒 = 1000ms
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return 0; // 如果时间已过期,返回0
|
|
114
|
+
};
|