@arcblock/ux 2.13.7 → 2.13.9

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 (37) hide show
  1. package/lib/UserCard/Cards/avatar-only.d.ts +3 -2
  2. package/lib/UserCard/Cards/basic-info.d.ts +3 -2
  3. package/lib/UserCard/Cards/basic-info.js +12 -6
  4. package/lib/UserCard/Cards/index.d.ts +3 -2
  5. package/lib/UserCard/Cards/name-only.d.ts +4 -2
  6. package/lib/UserCard/Cards/name-only.js +3 -2
  7. package/lib/UserCard/Container/dialog.js +1 -1
  8. package/lib/UserCard/Content/basic.d.ts +2 -1
  9. package/lib/UserCard/Content/basic.js +255 -68
  10. package/lib/UserCard/Content/clock.js +18 -5
  11. package/lib/UserCard/Content/minimal.d.ts +2 -2
  12. package/lib/UserCard/Content/minimal.js +6 -2
  13. package/lib/UserCard/Content/tooltip-avatar.d.ts +4 -4
  14. package/lib/UserCard/Content/tooltip-avatar.js +4 -3
  15. package/lib/UserCard/components.d.ts +2 -2
  16. package/lib/UserCard/components.js +9 -3
  17. package/lib/UserCard/index.js +36 -4
  18. package/lib/UserCard/types.d.ts +7 -4
  19. package/lib/UserCard/utils.d.ts +2 -0
  20. package/lib/UserCard/utils.js +33 -0
  21. package/package.json +6 -6
  22. package/src/UserCard/Cards/avatar-only.tsx +3 -2
  23. package/src/UserCard/Cards/basic-info.tsx +17 -5
  24. package/src/UserCard/Cards/index.tsx +3 -2
  25. package/src/UserCard/Cards/name-only.tsx +4 -4
  26. package/src/UserCard/Container/dialog.tsx +1 -1
  27. package/src/UserCard/Content/basic.tsx +243 -58
  28. package/src/UserCard/Content/clock.tsx +22 -11
  29. package/src/UserCard/Content/minimal.tsx +6 -3
  30. package/src/UserCard/Content/tooltip-avatar.tsx +10 -5
  31. package/src/UserCard/components.tsx +17 -7
  32. package/src/UserCard/index.tsx +41 -3
  33. package/src/UserCard/types.ts +11 -4
  34. package/src/UserCard/utils.ts +33 -0
  35. package/lib/UserCard/Content/left-layout.d.ts +0 -16
  36. package/lib/UserCard/Content/left-layout.js +0 -33
  37. package/src/UserCard/Content/left-layout.tsx +0 -40
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRef } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { CardType } from './types';
4
4
  import AvatarOnlyCard from './Cards/avatar-only';
5
5
  import DetailedCard from './Cards';
6
6
  import DialogContainer from './Container/dialog';
7
7
  import CardContainer from './Container/card';
8
+ import Avatar from '../Avatar';
9
+ import { getUserByDid, isUserDid } from './utils';
8
10
 
9
11
  // 创建仅显示名称首字母的头像
10
12
  export function createNameOnlyAvatar(user) {
@@ -36,7 +38,33 @@ function UserCard(props) {
36
38
  // 默认规则:AvatarOnly模式下默认显示悬停卡片,Detailed模式下默认不显示
37
39
  const shouldShowHoverCard = showHoverCard !== undefined ? showHoverCard : cardType === CardType.AvatarOnly;
38
40
  const containerRef = useRef(null);
41
+ const [user, setUser] = useState(null);
42
+ useEffect(() => {
43
+ let isSubscribed = true;
44
+ if (props.user) {
45
+ setUser(props.user);
46
+ } else if (props.did && isUserDid(props.did) && !props.user) {
47
+ getUserByDid(props.did).then(_user => {
48
+ if (isSubscribed) {
49
+ setUser(_user);
50
+ }
51
+ });
52
+ }
53
+ return () => {
54
+ isSubscribed = false;
55
+ };
56
+ }, [props.did, props.user]);
39
57
 
58
+ // 如果不存在,则使用 did 渲染头像
59
+ if (!user) {
60
+ return /*#__PURE__*/_jsx(Avatar, {
61
+ did: props.did,
62
+ size: props.avatarSize,
63
+ ...props.avatarProps
64
+ });
65
+ }
66
+
67
+ // user 存在,则使用 user 渲染头像
40
68
  // 渲染卡片内容(用于Tooltip)
41
69
  const renderCardContent = () => {
42
70
  const _avatarProps = props.popupAvatarProps || props.avatarProps;
@@ -46,8 +74,10 @@ function UserCard(props) {
46
74
  children: /*#__PURE__*/_jsx(DetailedCard, {
47
75
  ...props,
48
76
  shouldShowHoverCard: false,
77
+ user: user,
49
78
  avatarProps: _avatarProps,
50
- shortenLabelProps: _shortenLabelProps
79
+ shortenLabelProps: _shortenLabelProps,
80
+ renderFields: props.popupRenderFields
51
81
  })
52
82
  });
53
83
  };
@@ -57,7 +87,8 @@ function UserCard(props) {
57
87
  return /*#__PURE__*/_jsx(AvatarOnlyCard, {
58
88
  ...props,
59
89
  shouldShowHoverCard: shouldShowHoverCard,
60
- renderCardContent: renderCardContent
90
+ renderCardContent: renderCardContent,
91
+ user: user
61
92
  });
62
93
  }
63
94
 
@@ -69,7 +100,8 @@ function UserCard(props) {
69
100
  children: /*#__PURE__*/_jsx(DetailedCard, {
70
101
  ...props,
71
102
  shouldShowHoverCard: shouldShowHoverCard,
72
- renderCardContent: renderCardContent
103
+ renderCardContent: renderCardContent,
104
+ user: user
73
105
  })
74
106
  });
75
107
  }
@@ -57,7 +57,6 @@ export type UserAddress = {
57
57
  postalCode?: string;
58
58
  };
59
59
  export type User = UserPublicInfo & {
60
- role: string;
61
60
  email?: string;
62
61
  phone?: string;
63
62
  sourceProvider?: string;
@@ -69,7 +68,7 @@ export type User = UserPublicInfo & {
69
68
  didSpace?: Record<string, any>;
70
69
  connectedAccounts?: any[];
71
70
  locale?: string;
72
- url: string;
71
+ url?: string;
73
72
  inviter?: string;
74
73
  emailVerified?: boolean;
75
74
  phoneVerified?: boolean;
@@ -92,7 +91,8 @@ export declare enum InfoType {
92
91
  Basic = "Basic"
93
92
  }
94
93
  export interface UserCardProps {
95
- user: User;
94
+ user?: User;
95
+ did?: string;
96
96
  cardType?: CardType;
97
97
  infoType?: InfoType;
98
98
  avatarSize?: number;
@@ -101,13 +101,16 @@ export interface UserCardProps {
101
101
  didProps?: Partial<DIDProps>;
102
102
  avatarProps?: Partial<AvatarProps>;
103
103
  popupAvatarProps?: Partial<AvatarProps>;
104
- tooltipProps?: Omit<TooltipProps, 'title'>;
104
+ tooltipProps?: Partial<TooltipProps>;
105
105
  sx?: SxProps<Theme>;
106
106
  popupSx?: SxProps<Theme>;
107
107
  shortenLabelProps?: Partial<ShortenLabelProps>;
108
108
  popupShortenLabelProps?: Partial<ShortenLabelProps>;
109
+ renderFields?: string[];
110
+ popupRenderFields?: string[];
109
111
  renderTopRightContent?: () => React.ReactNode;
110
112
  topRightMaxWidth?: number;
111
113
  renderCustomContent?: () => React.ReactNode;
114
+ onAvatarClick?: (user: User, e?: React.MouseEvent<HTMLDivElement>) => void;
112
115
  }
113
116
  export {};
@@ -1,2 +1,4 @@
1
1
  import { User } from './types';
2
2
  export declare function createNameOnlyAvatar(user: User): string | null;
3
+ export declare function isUserDid(did: string): boolean;
4
+ export declare function getUserByDid(did: string): Promise<User | null>;
@@ -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.7",
3
+ "version": "2.13.9",
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": "9db5da4c6bc8ec9bd239bcd6d080930b52601bd2",
73
+ "gitHead": "a5aa684d78543947c92666fd6d461fade2a46097",
74
74
  "dependencies": {
75
75
  "@arcblock/did-motif": "^1.1.13",
76
- "@arcblock/icons": "^2.13.7",
77
- "@arcblock/nft-display": "^2.13.7",
78
- "@arcblock/react-hooks": "^2.13.7",
76
+ "@arcblock/icons": "^2.13.9",
77
+ "@arcblock/nft-display": "^2.13.9",
78
+ "@arcblock/react-hooks": "^2.13.9",
79
79
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
80
- "@blocklet/theme": "^2.13.7",
80
+ "@blocklet/theme": "^2.13.9",
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 { user, avatarSize = 40, renderCustomContent, isFull = true, infoType = InfoType.Minimal, ...rest } = props;
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
- {renderCustomContent && <Box sx={{ mt: 1.5 }}>{renderCustomContent()}</Box>}
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
  );
@@ -23,7 +23,7 @@ function DialogContainer({ children, sx }: DialogContainerProps) {
23
23
  border: '1px solid',
24
24
  borderColor: 'divider',
25
25
  borderRadius: 2,
26
- maxWidth: 400,
26
+ maxWidth: 500,
27
27
  minWidth: 320,
28
28
  display: 'flex',
29
29
  },
@@ -1,15 +1,28 @@
1
- import { Typography, Box } from '@mui/material';
2
- import { useCreation } from 'ahooks';
1
+ import { Typography, Box, Grid } from '@mui/material';
2
+ import { useCreation, useMemoizedFn } 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,89 @@ const iconSize = {
25
38
  interface BasicContentProps {
26
39
  user: User;
27
40
  isFull?: boolean;
41
+ renderFields?: string[];
42
+ }
43
+
44
+ // 定义渲染项的类型
45
+ interface RenderItem {
46
+ field: string;
47
+ value: string | any;
48
+ key: string;
49
+ }
50
+
51
+ function TimeZoneField({ value }: { value: string }) {
52
+ return (
53
+ <Box display="flex" alignItems="center" gap={1} className="user-card__timezone-field">
54
+ <TimezoneIcon {...iconSize} />
55
+ <Clock value={value} variant="body2" color="grey.800" />
56
+ </Box>
57
+ );
28
58
  }
29
59
 
30
- function BasicContent({ user, isFull = false }: BasicContentProps) {
60
+ function LinkField({ value }: { value: string }) {
61
+ const [useFallback, setUseFallback] = useState(false);
62
+ const faviconUrl = useCreation(() => {
63
+ try {
64
+ const url = new URL(value);
65
+ return joinURL(url.origin, 'favicon.ico');
66
+ } catch (e) {
67
+ return '';
68
+ }
69
+ }, [value]);
70
+
71
+ const handleImageError = () => {
72
+ setUseFallback(true);
73
+ };
74
+
75
+ return (
76
+ <Box display="flex" alignItems="center" gap={1} className="user-card__link-field">
77
+ {faviconUrl && !useFallback ? (
78
+ <img
79
+ src={faviconUrl}
80
+ alt="site icon"
81
+ style={{ width: 16, height: 16, objectFit: 'contain' }}
82
+ onError={handleImageError}
83
+ />
84
+ ) : (
85
+ <LinkIcon {...iconSize} />
86
+ )}
87
+ <LineText variant="body2" color="grey.800">
88
+ <Typography
89
+ component="a"
90
+ href={value}
91
+ style={{ textDecoration: 'none', color: 'inherit', fontSize: 'inherit' }}
92
+ target="_blank"
93
+ variant="body2"
94
+ color="grey.800"
95
+ rel="noopener noreferrer">
96
+ {formatLinkDisplay(value)}
97
+ </Typography>
98
+ </LineText>
99
+ </Box>
100
+ );
101
+ }
102
+
103
+ function BasicField({ field, value, children }: { field: string; value: string; children?: React.ReactNode }) {
104
+ const Icon = IconMap[field as keyof typeof IconMap];
105
+ return (
106
+ <Box key={field} display="flex" alignItems="center" gap={1} className={`user-card__${field}-field`}>
107
+ {Icon ? <Icon {...iconSize} /> : <IconifyIcon icon={infoCircleIcon} {...iconSize} />}
108
+ <LineText variant="body2" color="grey.800">
109
+ {children ?? value}
110
+ </LineText>
111
+ </Box>
112
+ );
113
+ }
114
+
115
+ function BasicContent({ user, isFull = false, renderFields }: BasicContentProps) {
116
+ const fields = useCreation(() => {
117
+ return renderFields ?? ['bio', 'email', 'phone', 'location', 'timezone', 'link'];
118
+ }, [renderFields]);
119
+
120
+ const includeBio = useCreation(() => {
121
+ return fields.includes('bio');
122
+ }, [fields]);
123
+
31
124
  const metadata = useCreation(() => {
32
125
  return (
33
126
  user.metadata ?? {
@@ -41,67 +134,159 @@ function BasicContent({ user, isFull = false }: BasicContentProps) {
41
134
  );
42
135
  }, [user]);
43
136
 
44
- // 获取第一个链接(如果有)
45
- const firstLink = metadata.links && metadata.links.length > 0 ? metadata.links[0] : null;
137
+ const address = useCreation(() => {
138
+ return user.address;
139
+ }, [user.address]);
46
140
 
47
- const moreContent = () => {
48
- return metadata.timezone ? (
49
- <Box display="flex" alignItems="center" gap={1}>
50
- <TimezoneIcon {...iconSize} />
51
- <LineText variant="body2" color="grey.800">
52
- <Clock value={metadata.timezone} variant="body2" color="grey.800" />
53
- </LineText>
54
- </Box>
55
- ) : null;
141
+ const getFieldValue = useCallback(
142
+ (field: string) => {
143
+ if (!field) return '';
144
+ switch (field) {
145
+ case 'bio':
146
+ return user.metadata?.bio || '';
147
+ case 'email':
148
+ return metadata.email || user.email || '';
149
+ case 'phone':
150
+ return metadata.phone?.phoneNumber || user.phone || '';
151
+ case 'location':
152
+ return address?.city || metadata.location || '';
153
+ case 'timezone':
154
+ return metadata.timezone || '';
155
+ case 'link':
156
+ return metadata.links?.map((link) => link.url).filter(Boolean) || [];
157
+ default:
158
+ return user[field as keyof User] || '';
159
+ }
160
+ },
161
+ [user, metadata, address]
162
+ );
163
+
164
+ // 计算实际可见的字段数量和渲染元素数量
165
+ const { effectiveRenderCount } = useMemo(() => {
166
+ const visible = fields.filter((field) => {
167
+ if (field === 'bio') return false; // bio field is handled separately
168
+ const value = getFieldValue(field);
169
+
170
+ if (value === undefined || value === null || value === '') return false;
171
+
172
+ if (isArray(value)) {
173
+ return value.length > 0;
174
+ }
175
+
176
+ return String(value).trim().length > 0;
177
+ });
178
+
179
+ // 计算实际渲染数量
180
+ const renderCount = visible.reduce((count, field) => {
181
+ const value = getFieldValue(field);
182
+ return count + (Array.isArray(value) ? value.filter((item) => item && String(item).trim()).length : 1);
183
+ }, 0);
184
+
185
+ return { visibleFields: visible, effectiveRenderCount: renderCount };
186
+ }, [fields, getFieldValue]);
187
+
188
+ // 判断是否需要使用两列布局
189
+ const useDoubleColumn = effectiveRenderCount > 4;
190
+
191
+ const isValidValue = useMemoizedFn(
192
+ (value: any) => value !== undefined && value !== null && String(value).trim().length > 0
193
+ );
194
+
195
+ // 准备要渲染的所有元素项
196
+ const renderItems = useMemo(() => {
197
+ const items: RenderItem[] = [];
198
+
199
+ // 遍历有效字段,展开数组字段为单独的渲染项
200
+ fields.forEach((field) => {
201
+ if (field === 'bio') return; // bio field is handled separately
202
+ const value = getFieldValue(field);
203
+
204
+ if (!value) return;
205
+
206
+ if (isArray(value)) {
207
+ // 数组类型字段,每个有效项生成一个渲染元素
208
+ value.forEach((item, index) => {
209
+ if (isValidValue(item)) {
210
+ items.push({
211
+ field,
212
+ value: item,
213
+ key: `${field}-${index}`,
214
+ });
215
+ }
216
+ });
217
+ } else if (isValidValue(value)) {
218
+ // 非数组类型字段,生成一个渲染元素
219
+ items.push({
220
+ field,
221
+ value,
222
+ key: field,
223
+ });
224
+ }
225
+ });
226
+
227
+ return items;
228
+ }, [fields, getFieldValue, isValidValue]);
229
+
230
+ if (fields.length === 0) {
231
+ return null;
232
+ }
233
+
234
+ const renderItem = (item: RenderItem) => {
235
+ const { field, value, key } = item;
236
+
237
+ switch (field) {
238
+ case 'link':
239
+ return <LinkField key={key} value={value as string} />;
240
+ case 'timezone':
241
+ return <TimeZoneField key={key} value={value as string} />;
242
+ case 'email':
243
+ return (
244
+ <BasicField
245
+ field={field}
246
+ value={value as string}
247
+ // eslint-disable-next-line react/no-children-prop
248
+ children={
249
+ <Typography
250
+ component="a"
251
+ href={`mailto:${value}`}
252
+ rel="noopener noreferrer"
253
+ referrerPolicy="no-referrer"
254
+ style={{
255
+ color: 'inherit',
256
+ textDecoration: 'none',
257
+ fontSize: 'inherit',
258
+ }}>
259
+ {value}
260
+ </Typography>
261
+ }
262
+ />
263
+ );
264
+ default:
265
+ return <BasicField key={key} field={field} value={value as string} />;
266
+ }
56
267
  };
57
268
 
58
269
  return (
59
- <Box mt={1} display="flex" flexDirection="column" gap={1.5}>
60
- {user.metadata?.bio && (
61
- <LineText variant="body2" color="grey.800">
270
+ <Box mt={1} display="flex" flexDirection="column" gap={1.5} className="user-card__basic-content">
271
+ {includeBio && user.metadata?.bio && (
272
+ <LineText variant="body2" color="grey.800" className="user-card__bio-field">
62
273
  {user.metadata.bio}
63
274
  </LineText>
64
275
  )}
65
- <Box display="flex" flexDirection="column" gap={0.5}>
66
- {/* 显示第一个链接(如果有) */}
67
- {firstLink && (
68
- <Box display="flex" alignItems="center" gap={1}>
69
- <LinkIcon {...iconSize} />
70
- <LineText>
71
- <Typography
72
- component="a"
73
- href={firstLink.url}
74
- style={{ textDecoration: 'none' }}
75
- target="_blank"
76
- variant="body2"
77
- color="grey.800"
78
- rel="noopener noreferrer">
79
- {formatLinkDisplay(firstLink.url)}
80
- </Typography>
81
- </LineText>
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>
276
+ {/* 其他字段 */}
277
+ {useDoubleColumn ? (
278
+ <Grid container spacing={0.5}>
279
+ {renderItems.map((item) => (
280
+ <Grid item xs={6} key={item.key}>
281
+ {renderItem(item)}
282
+ </Grid>
283
+ ))}
284
+ </Grid>
285
+ ) : (
286
+ <Box display="flex" flexDirection="column" gap={0.5}>
287
+ {renderItems.map((item) => renderItem(item))}
288
+ </Box>
289
+ )}
105
290
  </Box>
106
291
  );
107
292
  }
@@ -34,16 +34,17 @@ export default function Clock({ value, ...props }: { value: string; [key: string
34
34
  const timeInfo = useClock(value, locale);
35
35
 
36
36
  return (
37
- <Box
38
- sx={{
39
- whiteSpace: 'nowrap',
40
- overflow: 'hidden',
41
- textOverflow: 'ellipsis',
42
- }}
43
- display="flex"
44
- alignItems="center"
45
- gap={1}>
46
- <Typography {...props}>{value}</Typography>
37
+ <Box display="flex" alignItems="center" gap={1} flex={1} justifyContent="flex-start" overflow="hidden" width="100%">
38
+ <Typography
39
+ {...props}
40
+ noWrap
41
+ sx={{
42
+ flex: '0 1 auto',
43
+ minWidth: 0,
44
+ ...props.sx,
45
+ }}>
46
+ {value}
47
+ </Typography>
47
48
  <Tooltip
48
49
  enterDelay={200}
49
50
  title={
@@ -53,7 +54,17 @@ export default function Clock({ value, ...props }: { value: string; [key: string
53
54
  }
54
55
  placement="top"
55
56
  arrow>
56
- <Typography component="span" fontSize={14}>
57
+ <Typography
58
+ component="span"
59
+ fontSize={14}
60
+ noWrap
61
+ sx={{
62
+ whiteSpace: 'nowrap',
63
+ overflow: 'hidden',
64
+ textOverflow: 'ellipsis',
65
+ flex: '0 0 auto',
66
+ maxWidth: '100%',
67
+ }}>
57
68
  ({locale === 'zh' ? `${t(`timezonePhase.${timeInfo.phase}`)} ` : ''}
58
69
  {timeInfo.formattedTime})
59
70
  </Typography>