@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
@@ -1,24 +1,26 @@
1
- import { Box, CircularProgress, Tooltip, Typography } from '@mui/material';
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 RefreshOutlinedIcon from '@mui/icons-material/RefreshOutlined';
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 isEmpty from 'lodash/isEmpty';
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 { session } = useContext<TSessionContext>(SessionContext);
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
- const theme = useTheme();
47
+ useEffect(() => {
48
+ setUserStatus(user?.metadata?.status);
49
+ }, [user]);
46
50
 
47
- const isSmallView = theme.breakpoints.down('md');
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 refreshProfile = async () => {
78
+ const onSave = async (v: UserMetadata) => {
79
+ if (!isMyself) {
80
+ return;
81
+ }
54
82
  try {
55
- setLoading(true);
56
- await session.refreshProfile();
57
- session.refresh();
58
- Toast.success(translate(translations, 'refreshProfile.successfully', locale));
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
- <Avatar
75
- // @ts-ignore
76
- src={user?.avatar}
77
- did={user?.did}
78
- size={isSmallView ? 64 : 80}
79
- variant="circle"
80
- shape="circle"
81
- sx={{
82
- borderRadius: '100%',
83
- backgroundColor: '#fff',
84
- position: 'relative',
85
- overflow: 'hidden',
86
- flexShrink: 0,
87
- ...(isMyself
88
- ? {
89
- cursor: 'pointer',
90
- '&::after': {
91
- content: `"${t('switchProfile')}"`,
92
- color: 'white',
93
- position: 'absolute',
94
- fontSize: '12px',
95
- bottom: 0,
96
- left: 0,
97
- right: 0,
98
- height: '50%',
99
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
100
- display: 'flex',
101
- justifyContent: 'center',
102
- alignItems: 'center',
103
- },
104
- }
105
- : {}),
106
- }}
107
- onClick={isMyself ? switchProfile : noop}
108
- />
109
- <Box
110
- sx={{
111
- flex: 1,
112
- overflow: 'hidden',
113
- }}>
114
- <Typography
115
- variant="h6"
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
- fontWeight: 'bold',
118
- display: 'flex',
119
- alignItems: 'center',
120
- gap: 1,
121
- fontSize: '24px !important',
170
+ flex: 1,
171
+ overflow: 'hidden',
122
172
  }}>
123
- {user?.fullName}
124
- {isMyself ? (
125
- <>
126
- <SwitchRole user={user} switchPassport={switchPassport} />
127
- {!isEmpty(user.url) && (
128
- <Tooltip
129
- title={!loading && translate(translations, 'refreshProfile.title', locale)}
130
- sx={{ display: 'flex', alignItems: 'center' }}>
131
- {loading ? (
132
- <CircularProgress size="16px" sx={{ color: theme?.colors?.primary, ml: 0.5 }} />
133
- ) : (
134
- <RefreshOutlinedIcon onClick={refreshProfile} />
135
- )}
136
- </Tooltip>
137
- )}
138
- </>
139
- ) : null}
140
- </Typography>
141
- <DID did={user.did} showQrcode copyable compact={!showFullDid} responsive={!showFullDid} locale={locale} />
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 ? <DID did={user.inviter} showQrcode copyable compact responsive locale={locale} /> : '-',
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 }} verified={item.verified} />
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
+ };