@arcblock/ux 2.13.7 → 2.13.8
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/UserCard/Cards/avatar-only.d.ts +3 -2
- package/lib/UserCard/Cards/basic-info.d.ts +3 -2
- package/lib/UserCard/Cards/basic-info.js +12 -6
- package/lib/UserCard/Cards/index.d.ts +3 -2
- package/lib/UserCard/Cards/name-only.d.ts +4 -2
- package/lib/UserCard/Cards/name-only.js +3 -2
- package/lib/UserCard/Container/dialog.js +1 -1
- package/lib/UserCard/Content/basic.d.ts +2 -1
- package/lib/UserCard/Content/basic.js +195 -67
- package/lib/UserCard/Content/minimal.d.ts +2 -2
- package/lib/UserCard/Content/minimal.js +2 -0
- package/lib/UserCard/Content/tooltip-avatar.d.ts +4 -4
- package/lib/UserCard/Content/tooltip-avatar.js +4 -3
- package/lib/UserCard/components.d.ts +2 -2
- package/lib/UserCard/components.js +9 -3
- package/lib/UserCard/index.js +36 -4
- package/lib/UserCard/types.d.ts +7 -4
- package/lib/UserCard/utils.d.ts +2 -0
- package/lib/UserCard/utils.js +33 -0
- package/package.json +6 -6
- package/src/UserCard/Cards/avatar-only.tsx +3 -2
- package/src/UserCard/Cards/basic-info.tsx +17 -5
- package/src/UserCard/Cards/index.tsx +3 -2
- package/src/UserCard/Cards/name-only.tsx +4 -4
- package/src/UserCard/Container/dialog.tsx +1 -1
- package/src/UserCard/Content/basic.tsx +191 -57
- package/src/UserCard/Content/minimal.tsx +4 -3
- package/src/UserCard/Content/tooltip-avatar.tsx +10 -5
- package/src/UserCard/components.tsx +17 -7
- package/src/UserCard/index.tsx +41 -3
- package/src/UserCard/types.ts +11 -4
- package/src/UserCard/utils.ts +33 -0
package/lib/UserCard/utils.js
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
import { toTypeInfo } from '@arcblock/did';
|
2
|
+
import { types } from '@ocap/mcrypto';
|
3
|
+
import { BlockletSDK } from '@blocklet/js-sdk';
|
4
|
+
let client = null;
|
5
|
+
try {
|
6
|
+
client = new BlockletSDK();
|
7
|
+
} catch (error) {
|
8
|
+
console.error('Failed to initialize BlockletSDK:', error);
|
9
|
+
client = null;
|
10
|
+
}
|
11
|
+
|
1
12
|
// 创建仅显示名称首字母的头像
|
2
13
|
// eslint-disable-next-line import/prefer-default-export
|
3
14
|
export function createNameOnlyAvatar(user) {
|
@@ -16,4 +27,26 @@ export function createNameOnlyAvatar(user) {
|
|
16
27
|
content = user.did ? user.did.charAt(0).toUpperCase() : '?';
|
17
28
|
}
|
18
29
|
return content;
|
30
|
+
}
|
31
|
+
export function isUserDid(did) {
|
32
|
+
if (!did || typeof did !== 'string') return false;
|
33
|
+
try {
|
34
|
+
const didInfo = toTypeInfo(did);
|
35
|
+
return didInfo.role !== undefined && didInfo.role !== types.RoleType.ROLE_APPLICATION;
|
36
|
+
} catch (error) {
|
37
|
+
console.error('Failed to check if did is user did:', error);
|
38
|
+
return false;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
export async function getUserByDid(did) {
|
42
|
+
if (!client) return null;
|
43
|
+
try {
|
44
|
+
const user = await client.user.getUserPublicInfo({
|
45
|
+
did
|
46
|
+
});
|
47
|
+
return user;
|
48
|
+
} catch (error) {
|
49
|
+
console.error('Failed to get user by did:', error);
|
50
|
+
return null;
|
51
|
+
}
|
19
52
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.13.
|
3
|
+
"version": "2.13.8",
|
4
4
|
"description": "Common used react components for arcblock products",
|
5
5
|
"keywords": [
|
6
6
|
"react",
|
@@ -70,14 +70,14 @@
|
|
70
70
|
"react": ">=18.2.0",
|
71
71
|
"react-router-dom": ">=6.22.3"
|
72
72
|
},
|
73
|
-
"gitHead": "
|
73
|
+
"gitHead": "86ba86fce59551ceb09ee3cbdfd4646de2a69900",
|
74
74
|
"dependencies": {
|
75
75
|
"@arcblock/did-motif": "^1.1.13",
|
76
|
-
"@arcblock/icons": "^2.13.
|
77
|
-
"@arcblock/nft-display": "^2.13.
|
78
|
-
"@arcblock/react-hooks": "^2.13.
|
76
|
+
"@arcblock/icons": "^2.13.8",
|
77
|
+
"@arcblock/nft-display": "^2.13.8",
|
78
|
+
"@arcblock/react-hooks": "^2.13.8",
|
79
79
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
80
|
-
"@blocklet/theme": "^2.13.
|
80
|
+
"@blocklet/theme": "^2.13.8",
|
81
81
|
"@fontsource/roboto": "~5.1.1",
|
82
82
|
"@fontsource/ubuntu-mono": "^5.0.18",
|
83
83
|
"@iconify-icons/logos": "^1.2.36",
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import { UserCardProps, InfoType } from '../types';
|
2
|
+
import { UserCardProps, InfoType, User } from '../types';
|
3
3
|
import TooltipAvatar from '../Content/tooltip-avatar';
|
4
4
|
|
5
|
-
interface AvatarOnlyCardProps extends UserCardProps {
|
5
|
+
interface AvatarOnlyCardProps extends Omit<UserCardProps, 'user'> {
|
6
|
+
user: User;
|
6
7
|
renderCardContent: () => React.ReactNode;
|
7
8
|
shouldShowHoverCard: boolean;
|
8
9
|
}
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { Box } from '@mui/material';
|
3
|
-
import { InfoType, UserCardProps } from '../types';
|
3
|
+
import { InfoType, UserCardProps, User } from '../types';
|
4
4
|
import MinimalContent from '../Content/minimal';
|
5
5
|
import BasicContent from '../Content/basic';
|
6
6
|
|
7
|
-
interface BasicCardProps extends UserCardProps {
|
7
|
+
interface BasicCardProps extends Omit<UserCardProps, 'user'> {
|
8
|
+
user: User;
|
8
9
|
shouldShowHoverCard: boolean;
|
9
10
|
renderCardContent?: () => React.ReactNode | null;
|
10
11
|
isFull?: boolean;
|
@@ -12,14 +13,25 @@ interface BasicCardProps extends UserCardProps {
|
|
12
13
|
|
13
14
|
// 详细卡片模式下的Basic渲染组件
|
14
15
|
function BasicCard(props: BasicCardProps) {
|
15
|
-
const {
|
16
|
+
const {
|
17
|
+
user,
|
18
|
+
avatarSize = 40,
|
19
|
+
renderCustomContent,
|
20
|
+
isFull = true,
|
21
|
+
infoType = InfoType.Minimal,
|
22
|
+
renderFields,
|
23
|
+
popupRenderFields,
|
24
|
+
...rest
|
25
|
+
} = props;
|
16
26
|
|
17
27
|
return (
|
18
28
|
<Box display="flex" flexDirection="column" width="100%" sx={{ flex: 1, minWidth: 0 }}>
|
19
29
|
<MinimalContent user={user} avatarSize={avatarSize} {...rest} />
|
20
30
|
|
21
|
-
{infoType === InfoType.Basic && <BasicContent user={user} isFull={isFull} />}
|
22
|
-
|
31
|
+
{infoType === InfoType.Basic && <BasicContent user={user} isFull={isFull} renderFields={renderFields} />}
|
32
|
+
<Box className="user-card__footer">
|
33
|
+
{renderCustomContent && <Box sx={{ mt: 1.5 }}>{renderCustomContent()}</Box>}
|
34
|
+
</Box>
|
23
35
|
</Box>
|
24
36
|
);
|
25
37
|
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import { UserCardProps, InfoType } from '../types';
|
2
|
+
import { UserCardProps, InfoType, User } from '../types';
|
3
3
|
import NameOnlyCard from './name-only';
|
4
4
|
import BasicCard from './basic-info';
|
5
5
|
|
6
|
-
interface DetailedCardProps extends UserCardProps {
|
6
|
+
interface DetailedCardProps extends Omit<UserCardProps, 'user'> {
|
7
|
+
user: User;
|
7
8
|
shouldShowHoverCard: boolean;
|
8
9
|
renderCardContent?: () => React.ReactNode | null;
|
9
10
|
}
|
@@ -1,14 +1,14 @@
|
|
1
1
|
import { Typography } from '@mui/material';
|
2
|
-
import { UserCardProps } from '../types';
|
2
|
+
import { UserCardProps, User } from '../types';
|
3
3
|
import { renderAvatar } from '../components';
|
4
4
|
|
5
5
|
// 详细卡片模式下的NameOnly渲染组件
|
6
|
-
function NameOnlyCard(props: UserCardProps) {
|
7
|
-
const { user, avatarSize = 48 } = props;
|
6
|
+
function NameOnlyCard(props: Omit<UserCardProps, 'user'> & { user: User }) {
|
7
|
+
const { user, avatarSize = 48, onAvatarClick } = props;
|
8
8
|
|
9
9
|
return (
|
10
10
|
<>
|
11
|
-
{renderAvatar(user, avatarSize, props.avatarProps)}
|
11
|
+
{renderAvatar(user, avatarSize, props.avatarProps, onAvatarClick)}
|
12
12
|
<Typography variant="body1">{user.fullName || user.email || user.did}</Typography>
|
13
13
|
</>
|
14
14
|
);
|
@@ -1,15 +1,28 @@
|
|
1
|
-
import { Typography, Box } from '@mui/material';
|
1
|
+
import { Typography, Box, Grid } from '@mui/material';
|
2
2
|
import { useCreation } from 'ahooks';
|
3
3
|
import styled from '@emotion/styled';
|
4
|
+
import isArray from 'lodash/isArray';
|
5
|
+
import { Icon as IconifyIcon } from '@iconify/react';
|
6
|
+
import infoCircleIcon from '@iconify-icons/tabler/info-circle';
|
4
7
|
import LinkIcon from '@arcblock/icons/lib/Link';
|
8
|
+
import PhoneIcon from '@arcblock/icons/lib/Phone';
|
5
9
|
import LocationIcon from '@arcblock/icons/lib/Location';
|
6
10
|
import EmailIcon from '@arcblock/icons/lib/Email';
|
7
11
|
import TimezoneIcon from '@arcblock/icons/lib/Timezone';
|
8
|
-
import { withoutProtocol } from 'ufo';
|
12
|
+
import { joinURL, withoutProtocol } from 'ufo';
|
13
|
+
import { Fragment, useState, useMemo, useCallback } from 'react';
|
9
14
|
|
10
15
|
import { User } from '../types';
|
11
16
|
import Clock from './clock';
|
12
17
|
|
18
|
+
const IconMap = {
|
19
|
+
timezone: TimezoneIcon,
|
20
|
+
email: EmailIcon,
|
21
|
+
phone: PhoneIcon,
|
22
|
+
location: LocationIcon,
|
23
|
+
link: LinkIcon,
|
24
|
+
};
|
25
|
+
|
13
26
|
/**
|
14
27
|
* 格式化链接显示
|
15
28
|
* 对于 http/https 协议只显示域名,其他协议显示完整链接
|
@@ -25,9 +38,84 @@ const iconSize = {
|
|
25
38
|
interface BasicContentProps {
|
26
39
|
user: User;
|
27
40
|
isFull?: boolean;
|
41
|
+
renderFields?: string[];
|
42
|
+
}
|
43
|
+
|
44
|
+
function TimeZoneField({ value }: { value: string }) {
|
45
|
+
return (
|
46
|
+
<Box display="flex" alignItems="center" gap={1} className="user-card__timezone-field">
|
47
|
+
<TimezoneIcon {...iconSize} />
|
48
|
+
<LineText variant="body2" color="grey.800">
|
49
|
+
<Clock value={value} variant="body2" color="grey.800" />
|
50
|
+
</LineText>
|
51
|
+
</Box>
|
52
|
+
);
|
53
|
+
}
|
54
|
+
|
55
|
+
function LinkField({ value }: { value: string }) {
|
56
|
+
const [useFallback, setUseFallback] = useState(false);
|
57
|
+
const faviconUrl = useCreation(() => {
|
58
|
+
try {
|
59
|
+
const url = new URL(value);
|
60
|
+
return joinURL(url.origin, 'favicon.ico');
|
61
|
+
} catch (e) {
|
62
|
+
return '';
|
63
|
+
}
|
64
|
+
}, [value]);
|
65
|
+
|
66
|
+
const handleImageError = () => {
|
67
|
+
setUseFallback(true);
|
68
|
+
};
|
69
|
+
|
70
|
+
return (
|
71
|
+
<Box display="flex" alignItems="center" gap={1}>
|
72
|
+
{faviconUrl && !useFallback ? (
|
73
|
+
<img
|
74
|
+
src={faviconUrl}
|
75
|
+
alt="site icon"
|
76
|
+
style={{ width: 14, height: 14, objectFit: 'contain' }}
|
77
|
+
onError={handleImageError}
|
78
|
+
/>
|
79
|
+
) : (
|
80
|
+
<LinkIcon {...iconSize} />
|
81
|
+
)}
|
82
|
+
<LineText>
|
83
|
+
<Typography
|
84
|
+
component="a"
|
85
|
+
href={value}
|
86
|
+
style={{ textDecoration: 'none' }}
|
87
|
+
target="_blank"
|
88
|
+
variant="body2"
|
89
|
+
color="grey.800"
|
90
|
+
rel="noopener noreferrer">
|
91
|
+
{formatLinkDisplay(value)}
|
92
|
+
</Typography>
|
93
|
+
</LineText>
|
94
|
+
</Box>
|
95
|
+
);
|
96
|
+
}
|
97
|
+
|
98
|
+
function BasicField({ field, value }: { field: string; value: string }) {
|
99
|
+
const Icon = IconMap[field as keyof typeof IconMap];
|
100
|
+
return (
|
101
|
+
<Box key={field} display="flex" alignItems="center" gap={1} className={`user-card__${field}-field`}>
|
102
|
+
{Icon ? <Icon {...iconSize} /> : <IconifyIcon icon={infoCircleIcon} {...iconSize} />}
|
103
|
+
<LineText variant="body2" color="grey.800">
|
104
|
+
{value}
|
105
|
+
</LineText>
|
106
|
+
</Box>
|
107
|
+
);
|
28
108
|
}
|
29
109
|
|
30
|
-
function BasicContent({ user, isFull = false }: BasicContentProps) {
|
110
|
+
function BasicContent({ user, isFull = false, renderFields }: BasicContentProps) {
|
111
|
+
const fields = useCreation(() => {
|
112
|
+
return renderFields ?? ['bio', 'email', 'phone', 'location', 'timezone', 'link'];
|
113
|
+
}, [renderFields]);
|
114
|
+
|
115
|
+
const includeBio = useCreation(() => {
|
116
|
+
return fields.includes('bio');
|
117
|
+
}, [fields]);
|
118
|
+
|
31
119
|
const metadata = useCreation(() => {
|
32
120
|
return (
|
33
121
|
user.metadata ?? {
|
@@ -41,67 +129,113 @@ function BasicContent({ user, isFull = false }: BasicContentProps) {
|
|
41
129
|
);
|
42
130
|
}, [user]);
|
43
131
|
|
44
|
-
|
45
|
-
|
132
|
+
const address = useCreation(() => {
|
133
|
+
return user.address;
|
134
|
+
}, [user.address]);
|
46
135
|
|
47
|
-
const
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
136
|
+
const getFieldValue = useCallback(
|
137
|
+
(field: string) => {
|
138
|
+
if (!field) return '';
|
139
|
+
switch (field) {
|
140
|
+
case 'bio':
|
141
|
+
return user.metadata?.bio || '';
|
142
|
+
case 'email':
|
143
|
+
return metadata.email || user.email || '';
|
144
|
+
case 'phone':
|
145
|
+
return metadata.phone?.phoneNumber || user.phone || '';
|
146
|
+
case 'location':
|
147
|
+
return address?.city || metadata.location || '';
|
148
|
+
case 'timezone':
|
149
|
+
return metadata.timezone || '';
|
150
|
+
case 'link':
|
151
|
+
return metadata.links?.map((link) => link.url).filter(Boolean) || [];
|
152
|
+
default:
|
153
|
+
return user[field as keyof User] || '';
|
154
|
+
}
|
155
|
+
},
|
156
|
+
[user, metadata, address]
|
157
|
+
);
|
158
|
+
|
159
|
+
// 计算实际可见的字段数量
|
160
|
+
const visibleFields = useMemo(() => {
|
161
|
+
return fields.filter((field) => {
|
162
|
+
if (field === 'bio') return false; // bio field is handled separately
|
163
|
+
const value = getFieldValue(field);
|
164
|
+
|
165
|
+
if (value === undefined || value === null || value === '') return false;
|
166
|
+
|
167
|
+
if (isArray(value)) {
|
168
|
+
return value.length > 0;
|
169
|
+
}
|
170
|
+
|
171
|
+
return String(value).trim().length > 0;
|
172
|
+
});
|
173
|
+
}, [fields, getFieldValue]);
|
174
|
+
|
175
|
+
// 判断是否需要使用两列布局
|
176
|
+
const useDoubleColumn = visibleFields.length > 4;
|
177
|
+
|
178
|
+
if (fields.length === 0) {
|
179
|
+
return null;
|
180
|
+
}
|
181
|
+
|
182
|
+
const renderField = (field: string) => {
|
183
|
+
const value = getFieldValue(field);
|
184
|
+
if (!value || field === 'bio') {
|
185
|
+
return null;
|
186
|
+
}
|
187
|
+
|
188
|
+
if (isArray(value)) {
|
189
|
+
return (
|
190
|
+
<Fragment key={field}>
|
191
|
+
{field === 'link' ? (
|
192
|
+
<>
|
193
|
+
{value.map((link) => (
|
194
|
+
<LinkField key={link} value={link} />
|
195
|
+
))}
|
196
|
+
</>
|
197
|
+
) : (
|
198
|
+
<>
|
199
|
+
{value.map((item) => (
|
200
|
+
<BasicField key={item} field={field} value={item} />
|
201
|
+
))}
|
202
|
+
</>
|
203
|
+
)}
|
204
|
+
</Fragment>
|
205
|
+
);
|
206
|
+
}
|
207
|
+
|
208
|
+
if (field === 'timezone') {
|
209
|
+
return <TimeZoneField key={field} value={value as string} />;
|
210
|
+
}
|
211
|
+
|
212
|
+
return <BasicField key={field} field={field} value={value as string} />;
|
56
213
|
};
|
57
214
|
|
58
215
|
return (
|
59
|
-
<Box mt={1} display="flex" flexDirection="column" gap={1.5}>
|
60
|
-
{user.metadata?.bio && (
|
61
|
-
<LineText variant="body2" color="grey.800">
|
216
|
+
<Box mt={1} display="flex" flexDirection="column" gap={1.5} className="user-card__basic-content">
|
217
|
+
{includeBio && user.metadata?.bio && (
|
218
|
+
<LineText variant="body2" color="grey.800" className="user-card__bio-field">
|
62
219
|
{user.metadata.bio}
|
63
220
|
</LineText>
|
64
221
|
)}
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
<
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
</Box>
|
83
|
-
)}
|
84
|
-
{metadata.location && (
|
85
|
-
<Box display="flex" alignItems="center" gap={1}>
|
86
|
-
<LocationIcon {...iconSize} />
|
87
|
-
<LineText variant="body2" color="grey.800">
|
88
|
-
{metadata.location}
|
89
|
-
</LineText>
|
90
|
-
</Box>
|
91
|
-
)}
|
92
|
-
|
93
|
-
{isFull && moreContent()}
|
94
|
-
|
95
|
-
{/* 显示邮箱 */}
|
96
|
-
{(metadata.email || user.email) && (
|
97
|
-
<Box display="flex" alignItems="center" gap={1}>
|
98
|
-
<EmailIcon {...iconSize} />
|
99
|
-
<LineText variant="body2" color="grey.800">
|
100
|
-
{metadata.email || user.email}
|
101
|
-
</LineText>
|
102
|
-
</Box>
|
103
|
-
)}
|
104
|
-
</Box>
|
222
|
+
{/* 其他字段 */}
|
223
|
+
{useDoubleColumn ? (
|
224
|
+
<Grid container spacing={0.5}>
|
225
|
+
{fields.map((field) => {
|
226
|
+
if (field === 'bio' || !getFieldValue(field)) return null;
|
227
|
+
return (
|
228
|
+
<Grid item xs={6} key={field}>
|
229
|
+
{renderField(field)}
|
230
|
+
</Grid>
|
231
|
+
);
|
232
|
+
})}
|
233
|
+
</Grid>
|
234
|
+
) : (
|
235
|
+
<Box display="flex" flexDirection="column" gap={0.5}>
|
236
|
+
{fields.map((field) => renderField(field))}
|
237
|
+
</Box>
|
238
|
+
)}
|
105
239
|
</Box>
|
106
240
|
);
|
107
241
|
}
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import React, { memo } from 'react';
|
2
2
|
import { Typography, Box } from '@mui/material';
|
3
3
|
import DID from '../../DID';
|
4
|
-
import { UserCardProps } from '../types';
|
4
|
+
import { User, UserCardProps } from '../types';
|
5
5
|
import TooltipAvatar from './tooltip-avatar';
|
6
6
|
import { renderTopRight } from '../components';
|
7
7
|
import ShortenLabel from './shorten-label';
|
8
8
|
|
9
9
|
interface MinimalContentProps extends UserCardProps {
|
10
|
-
user:
|
10
|
+
user: User;
|
11
11
|
avatarSize: number;
|
12
12
|
shouldShowHoverCard: boolean;
|
13
13
|
renderCardContent?: () => React.ReactNode | null;
|
@@ -30,7 +30,7 @@ function MinimalContent(props: MinimalContentProps) {
|
|
30
30
|
} = props;
|
31
31
|
|
32
32
|
return (
|
33
|
-
<Box display="flex" justifyContent="space-between" alignItems="center">
|
33
|
+
<Box display="flex" justifyContent="space-between" alignItems="center" className="user-card__avatar-content">
|
34
34
|
<Box display="flex" justifyContent="flex-start" alignItems="center" gap={2} flex={1} minWidth={0}>
|
35
35
|
<TooltipAvatar
|
36
36
|
user={user}
|
@@ -44,6 +44,7 @@ function MinimalContent(props: MinimalContentProps) {
|
|
44
44
|
variant="subtitle1"
|
45
45
|
fontWeight={500}
|
46
46
|
color="text.primary"
|
47
|
+
className="user-card__full-name-label"
|
47
48
|
fontSize={18}
|
48
49
|
noWrap
|
49
50
|
sx={{ lineHeight: 1.1 }}>
|
@@ -2,11 +2,11 @@ import React from 'react';
|
|
2
2
|
import { Box, Tooltip } from '@mui/material';
|
3
3
|
|
4
4
|
import Zoom from '@mui/material/Zoom';
|
5
|
-
import { UserCardProps } from '../types';
|
5
|
+
import { User, UserCardProps } from '../types';
|
6
6
|
import { renderAvatar } from '../components';
|
7
7
|
|
8
|
-
interface TooltipAvatarProps {
|
9
|
-
user:
|
8
|
+
interface TooltipAvatarProps extends UserCardProps {
|
9
|
+
user: User;
|
10
10
|
avatarSize: number;
|
11
11
|
shouldShowHoverCard: boolean;
|
12
12
|
renderCardContent?: () => React.ReactNode | null;
|
@@ -27,8 +27,13 @@ function TooltipAvatar({
|
|
27
27
|
tooltipTitle,
|
28
28
|
tooltipProps,
|
29
29
|
avatarProps,
|
30
|
+
onAvatarClick,
|
30
31
|
}: TooltipAvatarProps) {
|
31
|
-
const avatarElement =
|
32
|
+
const avatarElement = (
|
33
|
+
<Box display="inline-block">
|
34
|
+
{renderAvatar(user, avatarSize, avatarProps, onAvatarClick, shouldShowHoverCard || !!tooltipTitle)}
|
35
|
+
</Box>
|
36
|
+
);
|
32
37
|
// 使用普通文本Tooltip
|
33
38
|
if (tooltipTitle) {
|
34
39
|
return (
|
@@ -52,7 +57,7 @@ function TooltipAvatar({
|
|
52
57
|
'& .MuiTooltip-tooltip': {
|
53
58
|
backgroundColor: 'transparent',
|
54
59
|
p: 0,
|
55
|
-
maxWidth:
|
60
|
+
maxWidth: 500,
|
56
61
|
zIndex: 1000,
|
57
62
|
},
|
58
63
|
},
|
@@ -1,27 +1,32 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { Box } from '@mui/material';
|
3
3
|
import Avatar from '../Avatar';
|
4
|
-
import { UserCardProps } from './types';
|
4
|
+
import { User, UserCardProps } from './types';
|
5
5
|
import { createNameOnlyAvatar } from './utils';
|
6
6
|
|
7
7
|
// 渲染头像
|
8
8
|
export const renderAvatar = (
|
9
|
-
user:
|
9
|
+
user: User,
|
10
10
|
avatarSize: number = 48,
|
11
|
-
avatarProps: UserCardProps['avatarProps'] = undefined
|
11
|
+
avatarProps: UserCardProps['avatarProps'] = undefined,
|
12
|
+
onAvatarClick: UserCardProps['onAvatarClick'] = undefined,
|
13
|
+
shouldShowHoverCard: boolean = false
|
12
14
|
) => {
|
15
|
+
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
16
|
+
onAvatarClick?.(user, e);
|
17
|
+
};
|
13
18
|
// 如果用户没有头像,则显示名称首字母头像
|
14
19
|
if (!user.avatar) {
|
15
20
|
const avatarContent = createNameOnlyAvatar(user);
|
16
|
-
|
17
21
|
return (
|
18
22
|
<Avatar
|
19
23
|
size={avatarSize}
|
20
24
|
did={user.did}
|
21
25
|
variant="circle"
|
26
|
+
onClick={onClick}
|
22
27
|
sx={{
|
23
28
|
fontSize: avatarSize * 0.4,
|
24
|
-
cursor: 'pointer',
|
29
|
+
cursor: shouldShowHoverCard || onAvatarClick ? 'pointer' : 'default',
|
25
30
|
}}
|
26
31
|
{...(avatarProps || {})}>
|
27
32
|
{avatarContent}
|
@@ -36,8 +41,9 @@ export const renderAvatar = (
|
|
36
41
|
did={user.did}
|
37
42
|
variant="circle"
|
38
43
|
style={{
|
39
|
-
cursor: 'pointer',
|
44
|
+
cursor: shouldShowHoverCard || onAvatarClick ? 'pointer' : 'default',
|
40
45
|
}}
|
46
|
+
onClick={onClick}
|
41
47
|
src={user.avatar}
|
42
48
|
alt={user.fullName || ''}
|
43
49
|
{...(avatarProps || {})}
|
@@ -51,7 +57,11 @@ export const renderTopRight = (
|
|
51
57
|
topRightMaxWidth: number = 120
|
52
58
|
) => {
|
53
59
|
if (renderTopRightContent) {
|
54
|
-
return
|
60
|
+
return (
|
61
|
+
<Box sx={{ maxWidth: topRightMaxWidth }} className="user-card__top-right-content">
|
62
|
+
{renderTopRightContent()}
|
63
|
+
</Box>
|
64
|
+
);
|
55
65
|
}
|
56
66
|
|
57
67
|
return null;
|
package/src/UserCard/index.tsx
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
import { useRef } from 'react';
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
2
|
+
import { toTypeInfo } from '@arcblock/did';
|
2
3
|
import type { User } from './types';
|
3
4
|
import { UserCardProps, CardType } from './types';
|
4
5
|
import AvatarOnlyCard from './Cards/avatar-only';
|
5
6
|
import DetailedCard from './Cards';
|
6
7
|
import DialogContainer from './Container/dialog';
|
7
8
|
import CardContainer from './Container/card';
|
9
|
+
import Avatar from '../Avatar';
|
10
|
+
import { getUserByDid, isUserDid } from './utils';
|
8
11
|
|
9
12
|
// 创建仅显示名称首字母的头像
|
10
13
|
export function createNameOnlyAvatar(user: User) {
|
@@ -35,7 +38,30 @@ function UserCard(props: UserCardProps) {
|
|
35
38
|
const shouldShowHoverCard = showHoverCard !== undefined ? showHoverCard : cardType === CardType.AvatarOnly;
|
36
39
|
|
37
40
|
const containerRef = useRef<HTMLDivElement>(null);
|
41
|
+
const [user, setUser] = useState<User | null>(null);
|
38
42
|
|
43
|
+
useEffect(() => {
|
44
|
+
let isSubscribed = true;
|
45
|
+
if (props.user) {
|
46
|
+
setUser(props.user);
|
47
|
+
} else if (props.did && isUserDid(props.did) && !props.user) {
|
48
|
+
getUserByDid(props.did).then((_user) => {
|
49
|
+
if (isSubscribed) {
|
50
|
+
setUser(_user);
|
51
|
+
}
|
52
|
+
});
|
53
|
+
}
|
54
|
+
return () => {
|
55
|
+
isSubscribed = false;
|
56
|
+
};
|
57
|
+
}, [props.did, props.user]);
|
58
|
+
|
59
|
+
// 如果不存在,则使用 did 渲染头像
|
60
|
+
if (!user) {
|
61
|
+
return <Avatar did={props.did} size={props.avatarSize} {...props.avatarProps} />;
|
62
|
+
}
|
63
|
+
|
64
|
+
// user 存在,则使用 user 渲染头像
|
39
65
|
// 渲染卡片内容(用于Tooltip)
|
40
66
|
const renderCardContent = () => {
|
41
67
|
const _avatarProps = props.popupAvatarProps || props.avatarProps;
|
@@ -45,8 +71,10 @@ function UserCard(props: UserCardProps) {
|
|
45
71
|
<DetailedCard
|
46
72
|
{...props}
|
47
73
|
shouldShowHoverCard={false}
|
74
|
+
user={user!}
|
48
75
|
avatarProps={_avatarProps}
|
49
76
|
shortenLabelProps={_shortenLabelProps}
|
77
|
+
renderFields={props.popupRenderFields}
|
50
78
|
/>
|
51
79
|
</DialogContainer>
|
52
80
|
);
|
@@ -55,14 +83,24 @@ function UserCard(props: UserCardProps) {
|
|
55
83
|
// 根据卡片类型选择合适的组件
|
56
84
|
if (cardType === CardType.AvatarOnly) {
|
57
85
|
return (
|
58
|
-
<AvatarOnlyCard
|
86
|
+
<AvatarOnlyCard
|
87
|
+
{...props}
|
88
|
+
shouldShowHoverCard={shouldShowHoverCard}
|
89
|
+
renderCardContent={renderCardContent}
|
90
|
+
user={user!}
|
91
|
+
/>
|
59
92
|
);
|
60
93
|
}
|
61
94
|
|
62
95
|
// 详细卡片模式
|
63
96
|
return (
|
64
97
|
<CardContainer containerRef={containerRef} cardType={cardType} sx={props.sx}>
|
65
|
-
<DetailedCard
|
98
|
+
<DetailedCard
|
99
|
+
{...props}
|
100
|
+
shouldShowHoverCard={shouldShowHoverCard}
|
101
|
+
renderCardContent={renderCardContent}
|
102
|
+
user={user!}
|
103
|
+
/>
|
66
104
|
</CardContainer>
|
67
105
|
);
|
68
106
|
}
|