@blocklet/ui-react 2.12.28 → 2.12.30
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 +8 -0
- package/lib/UserCenter/components/editable-field.js +80 -32
- package/lib/UserCenter/components/status-dialog/index.js +1 -1
- package/lib/UserCenter/components/user-center.js +8 -0
- package/lib/UserCenter/components/user-info/address.d.ts +15 -0
- package/lib/UserCenter/components/user-info/address.js +114 -0
- package/lib/UserCenter/components/user-info/clock.js +2 -0
- package/lib/UserCenter/components/user-info/metadata.d.ts +5 -2
- package/lib/UserCenter/components/user-info/metadata.js +73 -5
- package/lib/UserCenter/components/user-info/user-basic-info.d.ts +2 -1
- package/lib/UserCenter/components/user-info/user-basic-info.js +7 -3
- package/lib/UserCenter/libs/locales.d.ts +18 -0
- package/lib/UserCenter/libs/locales.js +18 -0
- package/package.json +4 -4
- package/src/@types/index.ts +11 -0
- package/src/UserCenter/components/editable-field.tsx +68 -14
- package/src/UserCenter/components/status-dialog/index.tsx +1 -1
- package/src/UserCenter/components/user-center.tsx +9 -0
- package/src/UserCenter/components/user-info/address.tsx +121 -0
- package/src/UserCenter/components/user-info/clock.tsx +3 -1
- package/src/UserCenter/components/user-info/metadata.tsx +115 -14
- package/src/UserCenter/components/user-info/user-basic-info.tsx +10 -6
- package/src/UserCenter/libs/locales.ts +18 -0
package/lib/@types/index.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ export type UserMetadata = {
|
|
|
77
77
|
email?: string;
|
|
78
78
|
phone?: UserPhoneProps;
|
|
79
79
|
};
|
|
80
|
+
export type UserAddress = {
|
|
81
|
+
country?: string;
|
|
82
|
+
province?: string;
|
|
83
|
+
city?: string;
|
|
84
|
+
detailedAddress?: string;
|
|
85
|
+
postalCode?: string;
|
|
86
|
+
};
|
|
80
87
|
export type User = UserPublicInfo & {
|
|
81
88
|
role: string;
|
|
82
89
|
email?: string;
|
|
@@ -95,6 +102,7 @@ export type User = UserPublicInfo & {
|
|
|
95
102
|
emailVerified?: boolean;
|
|
96
103
|
phoneVerified?: boolean;
|
|
97
104
|
metadata?: UserMetadata;
|
|
105
|
+
address?: UserAddress;
|
|
98
106
|
};
|
|
99
107
|
export type UserCenterTab = {
|
|
100
108
|
value: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
2
3
|
import { Box, TextField, Typography, Tooltip } from "@mui/material";
|
|
3
4
|
import { useCreation, useMemoizedFn } from "ahooks";
|
|
4
5
|
import { temp as colors } from "@arcblock/ux/lib/Colors";
|
|
@@ -59,6 +60,16 @@ function EditableField({
|
|
|
59
60
|
const t = useMemoizedFn((key, data = {}) => {
|
|
60
61
|
return translate(translations, key, locale, "en", data);
|
|
61
62
|
});
|
|
63
|
+
const [mousePosition, setMousePosition] = useState(null);
|
|
64
|
+
const handleMouseEnter = (event) => {
|
|
65
|
+
setMousePosition({
|
|
66
|
+
mouseX: event.clientX,
|
|
67
|
+
mouseY: event.clientY
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const handleMouseLeave = () => {
|
|
71
|
+
setMousePosition(null);
|
|
72
|
+
};
|
|
62
73
|
const handleChange = useMemoizedFn((v) => {
|
|
63
74
|
if (onChange) {
|
|
64
75
|
onChange(v);
|
|
@@ -107,7 +118,7 @@ function EditableField({
|
|
|
107
118
|
helperText: errorMsg
|
|
108
119
|
}
|
|
109
120
|
),
|
|
110
|
-
/* @__PURE__ */ jsxs(
|
|
121
|
+
maxLength && maxLength > 0 ? /* @__PURE__ */ jsxs(
|
|
111
122
|
Typography,
|
|
112
123
|
{
|
|
113
124
|
position: "absolute",
|
|
@@ -124,46 +135,83 @@ function EditableField({
|
|
|
124
135
|
maxLength
|
|
125
136
|
]
|
|
126
137
|
}
|
|
127
|
-
)
|
|
138
|
+
) : null
|
|
128
139
|
] });
|
|
129
140
|
}, [value, handleChange, component, placeholder, rows, children]);
|
|
130
141
|
if (!canEdit && editable) {
|
|
131
142
|
return null;
|
|
132
143
|
}
|
|
133
144
|
if (!editable) {
|
|
134
|
-
return value ? /* @__PURE__ */ jsx(
|
|
135
|
-
|
|
145
|
+
return value ? /* @__PURE__ */ jsx(
|
|
146
|
+
Tooltip,
|
|
136
147
|
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
open: Boolean(mousePosition),
|
|
149
|
+
title: tooltip,
|
|
150
|
+
placement: "top",
|
|
151
|
+
arrow: true,
|
|
152
|
+
PopperProps: {
|
|
153
|
+
// 使用虚拟锚点元素,基于鼠标位置
|
|
154
|
+
anchorEl: mousePosition ? {
|
|
155
|
+
getBoundingClientRect: () => ({
|
|
156
|
+
top: mousePosition.mouseY,
|
|
157
|
+
left: mousePosition.mouseX,
|
|
158
|
+
right: mousePosition.mouseX,
|
|
159
|
+
bottom: mousePosition.mouseY,
|
|
160
|
+
width: 0,
|
|
161
|
+
height: 0,
|
|
162
|
+
x: mousePosition.mouseX,
|
|
163
|
+
y: mousePosition.mouseY,
|
|
164
|
+
toJSON: () => {
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
} : null,
|
|
168
|
+
// 设置偏移,使tooltip显示在鼠标上方
|
|
169
|
+
modifiers: [
|
|
170
|
+
{
|
|
171
|
+
name: "offset",
|
|
172
|
+
options: {
|
|
173
|
+
offset: [0, 10]
|
|
160
174
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
children: /* @__PURE__ */ jsxs(
|
|
179
|
+
Typography,
|
|
180
|
+
{
|
|
181
|
+
variant: "subtitle1",
|
|
182
|
+
component: "div",
|
|
183
|
+
gutterBottom: true,
|
|
184
|
+
sx: {
|
|
185
|
+
width: "100%",
|
|
186
|
+
mb: 0,
|
|
187
|
+
lineHeight: 1.4,
|
|
188
|
+
overflow: "hidden"
|
|
189
|
+
},
|
|
190
|
+
display: "flex",
|
|
191
|
+
alignItems: "center",
|
|
192
|
+
gap: 1,
|
|
193
|
+
children: [
|
|
194
|
+
icon,
|
|
195
|
+
/* @__PURE__ */ jsxs(Box, { display: "flex", flexDirection: "row", alignItems: "center", width: "90%", children: [
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
Typography,
|
|
198
|
+
{
|
|
199
|
+
sx: {
|
|
200
|
+
whiteSpace: "pre-wrap",
|
|
201
|
+
...inline ? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } : {}
|
|
202
|
+
},
|
|
203
|
+
onMouseEnter: handleMouseEnter,
|
|
204
|
+
onMouseLeave: handleMouseLeave,
|
|
205
|
+
children: renderValue ? renderValue(value) : value
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
verified && /* @__PURE__ */ jsx(VerifiedIcon, { color: "success", style: { fontSize: 16, width: 16, marginLeft: 4, flexShrink: 0 } })
|
|
209
|
+
] })
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
)
|
|
165
213
|
}
|
|
166
|
-
)
|
|
214
|
+
) : null;
|
|
167
215
|
}
|
|
168
216
|
return /* @__PURE__ */ jsxs(Box, { sx: { width: "100%" }, style, children: [
|
|
169
217
|
label && /* @__PURE__ */ jsx(Typography, { variant: "subtitle1", gutterBottom: true, sx: { mb: 0.5, fontSize: "12px", color: "#4B5563" }, children: label }),
|
|
@@ -96,7 +96,7 @@ export default function StatusDialog({ open, onClose, data, selected, onSelect,
|
|
|
96
96
|
},
|
|
97
97
|
children: [
|
|
98
98
|
/* @__PURE__ */ jsxs(DialogTitle, { sx: { borderBottom: `1px solid ${colors.dividerColor}` }, children: [
|
|
99
|
-
/* @__PURE__ */ jsx(Typography, { variant: "
|
|
99
|
+
/* @__PURE__ */ jsx(Typography, { variant: "body1", sx: { fontSize: "16px !important", mb: 0 }, children: t("profile.setStatus") }),
|
|
100
100
|
/* @__PURE__ */ jsx(
|
|
101
101
|
IconButton,
|
|
102
102
|
{
|
|
@@ -116,6 +116,12 @@ export default function UserCenter({
|
|
|
116
116
|
refreshDeps: [currentDid, isMyself, session?.initialized, session?.user]
|
|
117
117
|
}
|
|
118
118
|
);
|
|
119
|
+
const onRefreshUser = useMemoizedFn(() => {
|
|
120
|
+
if (isMyself) {
|
|
121
|
+
return session.refresh();
|
|
122
|
+
}
|
|
123
|
+
return userState.refresh();
|
|
124
|
+
});
|
|
119
125
|
const privacyState = useRequest(
|
|
120
126
|
async () => {
|
|
121
127
|
if (userState.data && currentTab) {
|
|
@@ -400,6 +406,7 @@ export default function UserCenter({
|
|
|
400
406
|
user: userState.data,
|
|
401
407
|
showFullDid: false,
|
|
402
408
|
onlyProfile,
|
|
409
|
+
refreshProfile: onRefreshUser,
|
|
403
410
|
sx: {
|
|
404
411
|
padding: !isMobile ? "40px 24px 24px 40px" : "16px 0 0 0",
|
|
405
412
|
...!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {},
|
|
@@ -473,6 +480,7 @@ export default function UserCenter({
|
|
|
473
480
|
switchPassport: handleSwitchPassport,
|
|
474
481
|
switchProfile: session.switchProfile,
|
|
475
482
|
user: userState.data,
|
|
483
|
+
refreshProfile: onRefreshUser,
|
|
476
484
|
showFullDid: false,
|
|
477
485
|
sx: {
|
|
478
486
|
padding: !isMobile ? "40px 24px 24px 40px" : "16px 0 0 0",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UserAddress } from '../../../@types';
|
|
2
|
+
interface AddressErrors {
|
|
3
|
+
country?: string;
|
|
4
|
+
province?: string;
|
|
5
|
+
city?: string;
|
|
6
|
+
detailedAddress?: string;
|
|
7
|
+
postalCode?: string;
|
|
8
|
+
}
|
|
9
|
+
export default function AddressEditor({ address, errors, handleChange, defaultCountry, }: {
|
|
10
|
+
address: UserAddress;
|
|
11
|
+
errors: AddressErrors;
|
|
12
|
+
handleChange: (field: keyof UserAddress, value: string) => void;
|
|
13
|
+
defaultCountry: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from "@mui/material";
|
|
3
|
+
import { useMemoizedFn } from "ahooks";
|
|
4
|
+
import { translate } from "@arcblock/ux/lib/Locale/util";
|
|
5
|
+
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
6
|
+
import { temp as colors } from "@arcblock/ux/lib/Colors";
|
|
7
|
+
import CountrySelect from "@arcblock/ux/lib/PhoneInput/country-select";
|
|
8
|
+
import { translations } from "../../libs/locales.js";
|
|
9
|
+
import EditableField from "../editable-field.js";
|
|
10
|
+
const selectStyle = {
|
|
11
|
+
width: "100%",
|
|
12
|
+
px: 2,
|
|
13
|
+
py: 1,
|
|
14
|
+
borderRadius: 1,
|
|
15
|
+
"&:hover": {
|
|
16
|
+
"fieldset.MuiOutlinedInput-notchedOutline": {
|
|
17
|
+
borderColor: colors.dividerColor
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"fieldset.MuiOutlinedInput-notchedOutline": {
|
|
21
|
+
borderColor: colors.dividerColor,
|
|
22
|
+
borderRadius: 1
|
|
23
|
+
},
|
|
24
|
+
".MuiSelect-select": {
|
|
25
|
+
padding: "0 !important",
|
|
26
|
+
display: "flex",
|
|
27
|
+
alignItems: "center"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export default function AddressEditor({
|
|
31
|
+
address,
|
|
32
|
+
errors,
|
|
33
|
+
handleChange,
|
|
34
|
+
defaultCountry
|
|
35
|
+
}) {
|
|
36
|
+
const { locale } = useLocaleContext();
|
|
37
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
38
|
+
return translate(translations, key, locale, "en", data);
|
|
39
|
+
});
|
|
40
|
+
return /* @__PURE__ */ jsxs(Box, { display: "flex", flexDirection: "column", gap: 2, mt: 2, children: [
|
|
41
|
+
/* @__PURE__ */ jsx(
|
|
42
|
+
EditableField,
|
|
43
|
+
{
|
|
44
|
+
placeholder: t("profile.address.country"),
|
|
45
|
+
value: address.country || defaultCountry,
|
|
46
|
+
editable: true,
|
|
47
|
+
errorMsg: errors.country,
|
|
48
|
+
label: t("profile.address.country"),
|
|
49
|
+
children: /* @__PURE__ */ jsx(
|
|
50
|
+
CountrySelect,
|
|
51
|
+
{
|
|
52
|
+
valueField: "name",
|
|
53
|
+
value: address.country || defaultCountry,
|
|
54
|
+
onChange: (v) => handleChange("country", v),
|
|
55
|
+
displayEmpty: true,
|
|
56
|
+
variant: "outlined",
|
|
57
|
+
selectCountryProps: {
|
|
58
|
+
hideDialCode: true
|
|
59
|
+
},
|
|
60
|
+
sx: selectStyle
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
/* @__PURE__ */ jsxs(Box, { display: "flex", gap: 2, children: [
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
EditableField,
|
|
68
|
+
{
|
|
69
|
+
value: address.province || "",
|
|
70
|
+
onChange: (value) => handleChange("province", value),
|
|
71
|
+
placeholder: t("profile.address.province"),
|
|
72
|
+
label: t("profile.address.province"),
|
|
73
|
+
editable: true,
|
|
74
|
+
errorMsg: errors.province
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
EditableField,
|
|
79
|
+
{
|
|
80
|
+
value: address.city || "",
|
|
81
|
+
onChange: (value) => handleChange("city", value),
|
|
82
|
+
placeholder: t("profile.address.city"),
|
|
83
|
+
label: t("profile.address.city"),
|
|
84
|
+
editable: true,
|
|
85
|
+
errorMsg: errors.city
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
] }),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
EditableField,
|
|
91
|
+
{
|
|
92
|
+
value: address.detailedAddress || "",
|
|
93
|
+
onChange: (value) => handleChange("detailedAddress", value),
|
|
94
|
+
placeholder: t("profile.address.detailedAddress"),
|
|
95
|
+
label: t("profile.address.detailedAddress"),
|
|
96
|
+
component: "textarea",
|
|
97
|
+
editable: true,
|
|
98
|
+
rows: 3,
|
|
99
|
+
errorMsg: errors.detailedAddress
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
/* @__PURE__ */ jsx(
|
|
103
|
+
EditableField,
|
|
104
|
+
{
|
|
105
|
+
value: address.postalCode || "",
|
|
106
|
+
onChange: (value) => handleChange("postalCode", value),
|
|
107
|
+
placeholder: t("profile.address.postalCode"),
|
|
108
|
+
label: t("profile.address.postalCode"),
|
|
109
|
+
editable: true,
|
|
110
|
+
errorMsg: errors.postalCode
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
] });
|
|
114
|
+
}
|
|
@@ -34,6 +34,8 @@ export default function Clock({ value }) {
|
|
|
34
34
|
" ",
|
|
35
35
|
timeInfo.fullDateTime
|
|
36
36
|
] }),
|
|
37
|
+
placement: "top",
|
|
38
|
+
arrow: true,
|
|
37
39
|
children: /* @__PURE__ */ jsxs(Typography, { component: "span", fontSize: 14, children: [
|
|
38
40
|
"(",
|
|
39
41
|
locale === "zh" ? `${t(`profile.timezonePhase.${timeInfo.phase}`)} ` : "",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { User, UserMetadata } from '../../../@types';
|
|
1
|
+
import type { User, UserAddress, UserMetadata } from '../../../@types';
|
|
2
2
|
export default function UserMetadataComponent({ isMyself, user, onSave, isMobile, }: {
|
|
3
3
|
isMobile: boolean;
|
|
4
4
|
isMyself: boolean;
|
|
5
5
|
user: User;
|
|
6
|
-
onSave: (v:
|
|
6
|
+
onSave: (v: {
|
|
7
|
+
metadata: UserMetadata;
|
|
8
|
+
address: UserAddress;
|
|
9
|
+
}) => void;
|
|
7
10
|
}): import("react").JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { createElement } from "react";
|
|
|
3
3
|
import Box from "@mui/material/Box";
|
|
4
4
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
5
5
|
import SwipeableDrawer from "@mui/material/SwipeableDrawer";
|
|
6
|
+
import Typography from "@mui/material/Typography";
|
|
6
7
|
import Backdrop from "@mui/material/Backdrop";
|
|
7
8
|
import styled from "@emotion/styled";
|
|
8
9
|
import { joinURL } from "ufo";
|
|
@@ -14,6 +15,7 @@ import { useCreation, useMemoizedFn, useReactive } from "ahooks";
|
|
|
14
15
|
import { useMemo, useRef, useState, memo, forwardRef, useEffect, lazy } from "react";
|
|
15
16
|
import { translate } from "@arcblock/ux/lib/Locale/util";
|
|
16
17
|
import isEmail from "validator/lib/isEmail";
|
|
18
|
+
import isPostalCode from "validator/lib/isPostalCode";
|
|
17
19
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
18
20
|
import { useBrowser } from "@arcblock/react-hooks";
|
|
19
21
|
import { translations } from "../../libs/locales.js";
|
|
@@ -22,6 +24,7 @@ import { LinkPreviewInput } from "./link-preview-input.js";
|
|
|
22
24
|
import { currentTimezone, defaultButtonStyle, primaryButtonStyle } from "./utils.js";
|
|
23
25
|
import { TimezoneSelect } from "./timezone-select.js";
|
|
24
26
|
import Clock from "./clock.js";
|
|
27
|
+
import AddressEditor from "./address.js";
|
|
25
28
|
const LocationIcon = lazy(() => import("@arcblock/icons/lib/Location"));
|
|
26
29
|
const TimezoneIcon = lazy(() => import("@arcblock/icons/lib/Timezone"));
|
|
27
30
|
const EmailIcon = lazy(() => import("@arcblock/icons/lib/Email"));
|
|
@@ -65,6 +68,13 @@ export default function UserMetadataComponent({
|
|
|
65
68
|
email: "",
|
|
66
69
|
phone: ""
|
|
67
70
|
});
|
|
71
|
+
const addressValidateMsg = useReactive({
|
|
72
|
+
country: "",
|
|
73
|
+
province: "",
|
|
74
|
+
city: "",
|
|
75
|
+
detailedAddress: "",
|
|
76
|
+
postalCode: ""
|
|
77
|
+
});
|
|
68
78
|
useEffect(() => {
|
|
69
79
|
if (!isMobileView) {
|
|
70
80
|
setVisible(false);
|
|
@@ -90,6 +100,21 @@ export default function UserMetadataComponent({
|
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
);
|
|
103
|
+
const address = useCreation(() => {
|
|
104
|
+
return user?.address || {
|
|
105
|
+
country: "",
|
|
106
|
+
province: "",
|
|
107
|
+
city: "",
|
|
108
|
+
detailedAddress: "",
|
|
109
|
+
postalCode: ""
|
|
110
|
+
};
|
|
111
|
+
}, [user?.address]);
|
|
112
|
+
const defaultCountry = useMemo(() => {
|
|
113
|
+
if (user?.address?.country) {
|
|
114
|
+
return user.address.country;
|
|
115
|
+
}
|
|
116
|
+
return locale === "zh" ? "China" : "United States";
|
|
117
|
+
}, [user?.address?.country, locale]);
|
|
93
118
|
const phoneValue = useCreation(() => {
|
|
94
119
|
const phone = metadata.phone ?? user?.phone ?? {
|
|
95
120
|
country: "cn",
|
|
@@ -115,6 +140,17 @@ export default function UserMetadataComponent({
|
|
|
115
140
|
const onChange = (v, field) => {
|
|
116
141
|
metadata[field] = v;
|
|
117
142
|
};
|
|
143
|
+
const onAddressChange = (field, value) => {
|
|
144
|
+
address[field] = value;
|
|
145
|
+
if (field === "city") {
|
|
146
|
+
onChange(value, "location");
|
|
147
|
+
}
|
|
148
|
+
if (field === "postalCode") {
|
|
149
|
+
addressValidateMsg.postalCode = value && !isPostalCode(value, "any") ? t("profile.address.invalidPostalCode") : "";
|
|
150
|
+
} else {
|
|
151
|
+
addressValidateMsg[field] = "";
|
|
152
|
+
}
|
|
153
|
+
};
|
|
118
154
|
const onEdit = () => {
|
|
119
155
|
if (!isMobileView) {
|
|
120
156
|
setEditable(true);
|
|
@@ -130,8 +166,18 @@ export default function UserMetadataComponent({
|
|
|
130
166
|
metadata[k] = defaultMetadata[k];
|
|
131
167
|
});
|
|
132
168
|
}
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
const defaultAddress = cloneDeep(user?.address) ?? {};
|
|
170
|
+
if (defaultAddress) {
|
|
171
|
+
Object.keys(address).forEach((key) => {
|
|
172
|
+
const k = key;
|
|
173
|
+
address[k] = defaultAddress[k];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
[validateMsg, addressValidateMsg].forEach((o) => {
|
|
177
|
+
Object.keys(o).forEach((key) => {
|
|
178
|
+
o[key] = "";
|
|
179
|
+
});
|
|
180
|
+
});
|
|
135
181
|
if (!isMobileView) {
|
|
136
182
|
setEditable(false);
|
|
137
183
|
} else {
|
|
@@ -177,7 +223,13 @@ export default function UserMetadataComponent({
|
|
|
177
223
|
metadata[k] = value || currentTimezone;
|
|
178
224
|
}
|
|
179
225
|
});
|
|
180
|
-
|
|
226
|
+
if (address.postalCode && !isPostalCode(address.postalCode, "any")) {
|
|
227
|
+
addressValidateMsg.postalCode = t("profile.address.invalidPostalCode");
|
|
228
|
+
}
|
|
229
|
+
if ([validateMsg, addressValidateMsg].some((o) => Object.values(o).some((e) => e))) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
onSave({ metadata, address });
|
|
181
233
|
setEditable(false);
|
|
182
234
|
setVisible(false);
|
|
183
235
|
};
|
|
@@ -225,14 +277,30 @@ export default function UserMetadataComponent({
|
|
|
225
277
|
children: t("profile.editProfile")
|
|
226
278
|
}
|
|
227
279
|
) : null,
|
|
228
|
-
/* @__PURE__ */ jsx(
|
|
280
|
+
editing && isMyself ? /* @__PURE__ */ jsx(
|
|
281
|
+
AddressEditor,
|
|
282
|
+
{
|
|
283
|
+
address,
|
|
284
|
+
errors: addressValidateMsg,
|
|
285
|
+
handleChange: onAddressChange,
|
|
286
|
+
defaultCountry
|
|
287
|
+
}
|
|
288
|
+
) : /* @__PURE__ */ jsx(
|
|
229
289
|
EditableField,
|
|
230
290
|
{
|
|
231
|
-
value: metadata.location ?? "",
|
|
291
|
+
value: metadata.location ?? user?.address?.city ?? "",
|
|
232
292
|
onChange: (value) => onChange(value, "location"),
|
|
233
293
|
editable: editing,
|
|
234
294
|
placeholder: "Location",
|
|
235
295
|
label: t("profile.location"),
|
|
296
|
+
tooltip: isMyself ? /* @__PURE__ */ jsxs(Box, { fontSize: "14px", children: [
|
|
297
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", component: "p", fontWeight: 600, children: t("profile.address.detailedAddress") }),
|
|
298
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", component: "span", children: address.detailedAddress })
|
|
299
|
+
] }) : null,
|
|
300
|
+
renderValue: () => {
|
|
301
|
+
const fullLocation = [address.country, address.province, address.city || metadata.location || ""].filter(Boolean).join(" ");
|
|
302
|
+
return /* @__PURE__ */ jsx(Typography, { component: "span", children: fullLocation });
|
|
303
|
+
},
|
|
236
304
|
icon: /* @__PURE__ */ jsx(LocationIcon, { ...iconSize })
|
|
237
305
|
}
|
|
238
306
|
),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BoxProps } from '@mui/material';
|
|
2
2
|
import type { User } from '../../../@types';
|
|
3
|
-
export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassport, switchProfile, isMobile, onlyProfile, ...rest }: {
|
|
3
|
+
export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassport, switchProfile, isMobile, onlyProfile, refreshProfile, ...rest }: {
|
|
4
4
|
user: User;
|
|
5
5
|
isMyself?: boolean;
|
|
6
6
|
showFullDid?: boolean;
|
|
@@ -9,4 +9,5 @@ export default function UserBasicInfo({ user, isMyself, showFullDid, switchPassp
|
|
|
9
9
|
size?: number;
|
|
10
10
|
isMobile?: boolean;
|
|
11
11
|
onlyProfile?: boolean;
|
|
12
|
+
refreshProfile: () => void;
|
|
12
13
|
} & BoxProps): import("react").JSX.Element | null;
|
|
@@ -28,6 +28,7 @@ export default function UserBasicInfo({
|
|
|
28
28
|
switchProfile,
|
|
29
29
|
isMobile = false,
|
|
30
30
|
onlyProfile = false,
|
|
31
|
+
refreshProfile,
|
|
31
32
|
...rest
|
|
32
33
|
}) {
|
|
33
34
|
const { locale } = useLocaleContext();
|
|
@@ -59,6 +60,7 @@ export default function UserBasicInfo({
|
|
|
59
60
|
status: v || {}
|
|
60
61
|
}
|
|
61
62
|
});
|
|
63
|
+
refreshProfile();
|
|
62
64
|
} catch (err) {
|
|
63
65
|
console.error(err);
|
|
64
66
|
Toast.error(formatAxiosError(err));
|
|
@@ -74,8 +76,9 @@ export default function UserBasicInfo({
|
|
|
74
76
|
if (!isMyself) {
|
|
75
77
|
return;
|
|
76
78
|
}
|
|
79
|
+
const { metadata, address } = v;
|
|
77
80
|
try {
|
|
78
|
-
const newLinks =
|
|
81
|
+
const newLinks = metadata?.links?.map((link) => {
|
|
79
82
|
if (!link.url || !isValidUrl(link.url))
|
|
80
83
|
return null;
|
|
81
84
|
try {
|
|
@@ -89,8 +92,9 @@ export default function UserBasicInfo({
|
|
|
89
92
|
return null;
|
|
90
93
|
}
|
|
91
94
|
}).filter((l) => !!l) || [];
|
|
92
|
-
|
|
93
|
-
await client.user.saveProfile({ metadata
|
|
95
|
+
metadata.links = newLinks;
|
|
96
|
+
await client.user.saveProfile({ metadata, address });
|
|
97
|
+
refreshProfile();
|
|
94
98
|
} catch (err) {
|
|
95
99
|
console.error(err);
|
|
96
100
|
Toast.error(formatAxiosError(err));
|
|
@@ -139,6 +139,15 @@ export declare const translations: {
|
|
|
139
139
|
afternoon: string;
|
|
140
140
|
night: string;
|
|
141
141
|
};
|
|
142
|
+
address: {
|
|
143
|
+
title: string;
|
|
144
|
+
country: string;
|
|
145
|
+
province: string;
|
|
146
|
+
city: string;
|
|
147
|
+
detailedAddress: string;
|
|
148
|
+
postalCode: string;
|
|
149
|
+
invalidPostalCode: string;
|
|
150
|
+
};
|
|
142
151
|
};
|
|
143
152
|
};
|
|
144
153
|
en: {
|
|
@@ -282,6 +291,15 @@ export declare const translations: {
|
|
|
282
291
|
afternoon: string;
|
|
283
292
|
night: string;
|
|
284
293
|
};
|
|
294
|
+
address: {
|
|
295
|
+
title: string;
|
|
296
|
+
country: string;
|
|
297
|
+
province: string;
|
|
298
|
+
city: string;
|
|
299
|
+
detailedAddress: string;
|
|
300
|
+
postalCode: string;
|
|
301
|
+
invalidPostalCode: string;
|
|
302
|
+
};
|
|
285
303
|
};
|
|
286
304
|
};
|
|
287
305
|
};
|
|
@@ -138,6 +138,15 @@ export const translations = {
|
|
|
138
138
|
morning: "\u4E0A\u5348",
|
|
139
139
|
afternoon: "\u4E0B\u5348",
|
|
140
140
|
night: "\u665A\u4E0A"
|
|
141
|
+
},
|
|
142
|
+
address: {
|
|
143
|
+
title: "\u5730\u5740",
|
|
144
|
+
country: "\u56FD\u5BB6/\u5730\u533A",
|
|
145
|
+
province: "\u5DDE/\u7701",
|
|
146
|
+
city: "\u57CE\u5E02/\u9547",
|
|
147
|
+
detailedAddress: "\u8BE6\u7EC6\u5730\u5740",
|
|
148
|
+
postalCode: "\u90AE\u653F\u7F16\u7801",
|
|
149
|
+
invalidPostalCode: "\u90AE\u653F\u7F16\u7801\u683C\u5F0F\u4E0D\u6B63\u786E"
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
},
|
|
@@ -281,6 +290,15 @@ export const translations = {
|
|
|
281
290
|
morning: "AM",
|
|
282
291
|
afternoon: "PM",
|
|
283
292
|
night: "PM"
|
|
293
|
+
},
|
|
294
|
+
address: {
|
|
295
|
+
title: "Address",
|
|
296
|
+
country: "Country/Region",
|
|
297
|
+
province: "State/Province",
|
|
298
|
+
city: "City/Town",
|
|
299
|
+
detailedAddress: "Detailed Address",
|
|
300
|
+
postalCode: "Postal Code",
|
|
301
|
+
invalidPostalCode: "Postal code is invalid"
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/ui-react",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.30",
|
|
4
4
|
"description": "Some useful front-end web components that can be used in Blocklets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@abtnode/constant": "^1.16.40",
|
|
36
|
-
"@arcblock/bridge": "^2.12.
|
|
37
|
-
"@arcblock/react-hooks": "^2.12.
|
|
36
|
+
"@arcblock/bridge": "^2.12.30",
|
|
37
|
+
"@arcblock/react-hooks": "^2.12.30",
|
|
38
38
|
"@arcblock/ws": "^1.19.15",
|
|
39
39
|
"@blocklet/did-space-react": "^1.0.35",
|
|
40
40
|
"@iconify-icons/logos": "^1.2.36",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"jest": "^29.7.0",
|
|
88
88
|
"unbuild": "^2.0.0"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "97305a61162566787fbc38decf9d26dcdb556637"
|
|
91
91
|
}
|
package/src/@types/index.ts
CHANGED
|
@@ -89,6 +89,14 @@ export type UserMetadata = {
|
|
|
89
89
|
phone?: UserPhoneProps;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
export type UserAddress = {
|
|
93
|
+
country?: string;
|
|
94
|
+
province?: string;
|
|
95
|
+
city?: string;
|
|
96
|
+
detailedAddress?: string;
|
|
97
|
+
postalCode?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
92
100
|
export type User = UserPublicInfo & {
|
|
93
101
|
role: string;
|
|
94
102
|
email?: string;
|
|
@@ -106,7 +114,10 @@ export type User = UserPublicInfo & {
|
|
|
106
114
|
inviter?: string;
|
|
107
115
|
emailVerified?: boolean;
|
|
108
116
|
phoneVerified?: boolean;
|
|
117
|
+
// 1.16.40 新增
|
|
109
118
|
metadata?: UserMetadata;
|
|
119
|
+
// 1.16.41 新增
|
|
120
|
+
address?: UserAddress;
|
|
110
121
|
};
|
|
111
122
|
|
|
112
123
|
export type UserCenterTab = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Box, TextField, Typography, Tooltip, TooltipProps } from '@mui/material';
|
|
3
3
|
import { useCreation, useMemoizedFn } from 'ahooks';
|
|
4
4
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
@@ -82,6 +82,25 @@ function EditableField({
|
|
|
82
82
|
return translate(translations, key, locale, 'en', data);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
// 存储鼠标位置的状态
|
|
86
|
+
const [mousePosition, setMousePosition] = useState<{
|
|
87
|
+
mouseX: number;
|
|
88
|
+
mouseY: number;
|
|
89
|
+
} | null>(null);
|
|
90
|
+
|
|
91
|
+
// 处理鼠标移动事件,更新鼠标位置
|
|
92
|
+
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
|
|
93
|
+
setMousePosition({
|
|
94
|
+
mouseX: event.clientX,
|
|
95
|
+
mouseY: event.clientY,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// 处理鼠标离开事件,清除鼠标位置
|
|
100
|
+
const handleMouseLeave = () => {
|
|
101
|
+
setMousePosition(null);
|
|
102
|
+
};
|
|
103
|
+
|
|
85
104
|
const handleChange = useMemoizedFn((v: string) => {
|
|
86
105
|
if (onChange) {
|
|
87
106
|
onChange(v);
|
|
@@ -129,17 +148,19 @@ function EditableField({
|
|
|
129
148
|
error={Boolean(errorMsg)}
|
|
130
149
|
helperText={errorMsg}
|
|
131
150
|
/>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
{maxLength && maxLength > 0 ? (
|
|
152
|
+
<Typography
|
|
153
|
+
position="absolute"
|
|
154
|
+
bottom={-22}
|
|
155
|
+
right={2}
|
|
156
|
+
variant="caption"
|
|
157
|
+
fontSize="12px"
|
|
158
|
+
component="span"
|
|
159
|
+
color={isTooLong ? 'error' : 'text.secondary'}>
|
|
160
|
+
{isTooLong ? `(${t('profile.maxInputLength')} ${maxLength}) ` : ''}
|
|
161
|
+
{value.length} / {maxLength}
|
|
162
|
+
</Typography>
|
|
163
|
+
) : null}
|
|
143
164
|
</Box>
|
|
144
165
|
);
|
|
145
166
|
}, [value, handleChange, component, placeholder, rows, children]);
|
|
@@ -150,7 +171,38 @@ function EditableField({
|
|
|
150
171
|
|
|
151
172
|
if (!editable) {
|
|
152
173
|
return value ? (
|
|
153
|
-
<Tooltip
|
|
174
|
+
<Tooltip
|
|
175
|
+
open={Boolean(mousePosition)}
|
|
176
|
+
title={tooltip}
|
|
177
|
+
placement="top"
|
|
178
|
+
arrow
|
|
179
|
+
PopperProps={{
|
|
180
|
+
// 使用虚拟锚点元素,基于鼠标位置
|
|
181
|
+
anchorEl: mousePosition
|
|
182
|
+
? {
|
|
183
|
+
getBoundingClientRect: () => ({
|
|
184
|
+
top: mousePosition.mouseY,
|
|
185
|
+
left: mousePosition.mouseX,
|
|
186
|
+
right: mousePosition.mouseX,
|
|
187
|
+
bottom: mousePosition.mouseY,
|
|
188
|
+
width: 0,
|
|
189
|
+
height: 0,
|
|
190
|
+
x: mousePosition.mouseX,
|
|
191
|
+
y: mousePosition.mouseY,
|
|
192
|
+
toJSON: () => {},
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
: null,
|
|
196
|
+
// 设置偏移,使tooltip显示在鼠标上方
|
|
197
|
+
modifiers: [
|
|
198
|
+
{
|
|
199
|
+
name: 'offset',
|
|
200
|
+
options: {
|
|
201
|
+
offset: [0, 10],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
}}>
|
|
154
206
|
<Typography
|
|
155
207
|
variant="subtitle1"
|
|
156
208
|
component="div"
|
|
@@ -170,7 +222,9 @@ function EditableField({
|
|
|
170
222
|
sx={{
|
|
171
223
|
whiteSpace: 'pre-wrap',
|
|
172
224
|
...(inline ? { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } : {}),
|
|
173
|
-
}}
|
|
225
|
+
}}
|
|
226
|
+
onMouseEnter={handleMouseEnter}
|
|
227
|
+
onMouseLeave={handleMouseLeave}>
|
|
174
228
|
{renderValue ? renderValue(value) : value}
|
|
175
229
|
</Typography>
|
|
176
230
|
{verified && (
|
|
@@ -114,7 +114,7 @@ export default function StatusDialog({ open, onClose, data, selected, onSelect,
|
|
|
114
114
|
},
|
|
115
115
|
}}>
|
|
116
116
|
<DialogTitle sx={{ borderBottom: `1px solid ${colors.dividerColor}` }}>
|
|
117
|
-
<Typography variant="
|
|
117
|
+
<Typography variant="body1" sx={{ fontSize: '16px !important', mb: 0 }}>
|
|
118
118
|
{t('profile.setStatus')}
|
|
119
119
|
</Typography>
|
|
120
120
|
<IconButton
|
|
@@ -145,6 +145,13 @@ export default function UserCenter({
|
|
|
145
145
|
}
|
|
146
146
|
);
|
|
147
147
|
|
|
148
|
+
const onRefreshUser = useMemoizedFn(() => {
|
|
149
|
+
if (isMyself) {
|
|
150
|
+
return session.refresh();
|
|
151
|
+
}
|
|
152
|
+
return userState.refresh();
|
|
153
|
+
});
|
|
154
|
+
|
|
148
155
|
const privacyState = useRequest(
|
|
149
156
|
async () => {
|
|
150
157
|
if (userState.data && currentTab) {
|
|
@@ -470,6 +477,7 @@ export default function UserCenter({
|
|
|
470
477
|
user={userState.data as User}
|
|
471
478
|
showFullDid={false}
|
|
472
479
|
onlyProfile={onlyProfile}
|
|
480
|
+
refreshProfile={onRefreshUser}
|
|
473
481
|
sx={{
|
|
474
482
|
padding: !isMobile ? '40px 24px 24px 40px' : '16px 0 0 0',
|
|
475
483
|
...(!isMobile ? { width: 320, maxWidth: 320, flexShrink: 0 } : {}),
|
|
@@ -538,6 +546,7 @@ export default function UserCenter({
|
|
|
538
546
|
switchPassport={handleSwitchPassport}
|
|
539
547
|
switchProfile={session.switchProfile}
|
|
540
548
|
user={userState.data as User}
|
|
549
|
+
refreshProfile={onRefreshUser}
|
|
541
550
|
showFullDid={false}
|
|
542
551
|
sx={{
|
|
543
552
|
padding: !isMobile ? '40px 24px 24px 40px' : '16px 0 0 0',
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户地址组件
|
|
3
|
+
*/
|
|
4
|
+
import { Box } from '@mui/material';
|
|
5
|
+
import { useMemoizedFn } from 'ahooks';
|
|
6
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
7
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
|
+
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
9
|
+
import CountrySelect from '@arcblock/ux/lib/PhoneInput/country-select';
|
|
10
|
+
import type { UserAddress } from '../../../@types';
|
|
11
|
+
import { translations } from '../../libs/locales';
|
|
12
|
+
import EditableField from '../editable-field';
|
|
13
|
+
|
|
14
|
+
const selectStyle = {
|
|
15
|
+
width: '100%',
|
|
16
|
+
px: 2,
|
|
17
|
+
py: 1,
|
|
18
|
+
borderRadius: 1,
|
|
19
|
+
'&:hover': {
|
|
20
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
|
21
|
+
borderColor: colors.dividerColor,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
'fieldset.MuiOutlinedInput-notchedOutline': {
|
|
25
|
+
borderColor: colors.dividerColor,
|
|
26
|
+
borderRadius: 1,
|
|
27
|
+
},
|
|
28
|
+
'.MuiSelect-select': {
|
|
29
|
+
padding: '0 !important',
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface AddressErrors {
|
|
36
|
+
country?: string;
|
|
37
|
+
province?: string;
|
|
38
|
+
city?: string;
|
|
39
|
+
detailedAddress?: string;
|
|
40
|
+
postalCode?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function AddressEditor({
|
|
44
|
+
address,
|
|
45
|
+
errors,
|
|
46
|
+
handleChange,
|
|
47
|
+
defaultCountry,
|
|
48
|
+
}: {
|
|
49
|
+
address: UserAddress;
|
|
50
|
+
errors: AddressErrors;
|
|
51
|
+
handleChange: (field: keyof UserAddress, value: string) => void;
|
|
52
|
+
defaultCountry: string;
|
|
53
|
+
}) {
|
|
54
|
+
const { locale } = useLocaleContext();
|
|
55
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
56
|
+
return translate(translations, key, locale, 'en', data);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box display="flex" flexDirection="column" gap={2} mt={2}>
|
|
61
|
+
<EditableField
|
|
62
|
+
placeholder={t('profile.address.country')}
|
|
63
|
+
value={address.country || defaultCountry}
|
|
64
|
+
editable
|
|
65
|
+
errorMsg={errors.country}
|
|
66
|
+
label={t('profile.address.country')}>
|
|
67
|
+
<CountrySelect<'name'>
|
|
68
|
+
valueField="name"
|
|
69
|
+
value={address.country || defaultCountry}
|
|
70
|
+
onChange={(v) => handleChange('country', v)}
|
|
71
|
+
displayEmpty
|
|
72
|
+
variant="outlined"
|
|
73
|
+
selectCountryProps={{
|
|
74
|
+
hideDialCode: true,
|
|
75
|
+
}}
|
|
76
|
+
sx={selectStyle}
|
|
77
|
+
/>
|
|
78
|
+
</EditableField>
|
|
79
|
+
|
|
80
|
+
<Box display="flex" gap={2}>
|
|
81
|
+
<EditableField
|
|
82
|
+
value={address.province || ''}
|
|
83
|
+
onChange={(value) => handleChange('province', value)}
|
|
84
|
+
placeholder={t('profile.address.province')}
|
|
85
|
+
label={t('profile.address.province')}
|
|
86
|
+
editable
|
|
87
|
+
errorMsg={errors.province}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<EditableField
|
|
91
|
+
value={address.city || ''}
|
|
92
|
+
onChange={(value) => handleChange('city', value)}
|
|
93
|
+
placeholder={t('profile.address.city')}
|
|
94
|
+
label={t('profile.address.city')}
|
|
95
|
+
editable
|
|
96
|
+
errorMsg={errors.city}
|
|
97
|
+
/>
|
|
98
|
+
</Box>
|
|
99
|
+
|
|
100
|
+
<EditableField
|
|
101
|
+
value={address.detailedAddress || ''}
|
|
102
|
+
onChange={(value) => handleChange('detailedAddress', value)}
|
|
103
|
+
placeholder={t('profile.address.detailedAddress')}
|
|
104
|
+
label={t('profile.address.detailedAddress')}
|
|
105
|
+
component="textarea"
|
|
106
|
+
editable
|
|
107
|
+
rows={3}
|
|
108
|
+
errorMsg={errors.detailedAddress}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<EditableField
|
|
112
|
+
value={address.postalCode || ''}
|
|
113
|
+
onChange={(value) => handleChange('postalCode', value)}
|
|
114
|
+
placeholder={t('profile.address.postalCode')}
|
|
115
|
+
label={t('profile.address.postalCode')}
|
|
116
|
+
editable
|
|
117
|
+
errorMsg={errors.postalCode}
|
|
118
|
+
/>
|
|
119
|
+
</Box>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -30,7 +30,9 @@ export default function Clock({ value }: { value: string }) {
|
|
|
30
30
|
<span>
|
|
31
31
|
{t('profile.localTime')} {timeInfo.fullDateTime}
|
|
32
32
|
</span>
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
|
+
placement="top"
|
|
35
|
+
arrow>
|
|
34
36
|
<Typography component="span" fontSize={14}>
|
|
35
37
|
({locale === 'zh' ? `${t(`profile.timezonePhase.${timeInfo.phase}`)} ` : ''}
|
|
36
38
|
{timeInfo.formattedTime})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
3
|
-
|
|
4
3
|
import Box from '@mui/material/Box';
|
|
5
4
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
6
5
|
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
|
6
|
+
import Typography from '@mui/material/Typography';
|
|
7
7
|
import Backdrop, { BackdropProps } from '@mui/material/Backdrop';
|
|
8
8
|
|
|
9
9
|
import styled from '@emotion/styled';
|
|
@@ -17,15 +17,17 @@ import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
|
|
|
17
17
|
import { useMemo, useRef, useState, memo, forwardRef, useEffect, lazy } from 'react';
|
|
18
18
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
19
19
|
import isEmail from 'validator/lib/isEmail';
|
|
20
|
+
import isPostalCode from 'validator/lib/isPostalCode';
|
|
20
21
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
21
22
|
import { useBrowser } from '@arcblock/react-hooks';
|
|
22
23
|
import { translations } from '../../libs/locales';
|
|
23
|
-
import type { User, UserMetadata, UserPhoneProps } from '../../../@types';
|
|
24
|
+
import type { User, UserAddress, UserMetadata, UserPhoneProps } from '../../../@types';
|
|
24
25
|
import EditableField, { commonInputStyle, inputFieldStyle } from '../editable-field';
|
|
25
26
|
import { LinkPreviewInput } from './link-preview-input';
|
|
26
27
|
import { currentTimezone, defaultButtonStyle, primaryButtonStyle } from './utils';
|
|
27
28
|
import { TimezoneSelect } from './timezone-select';
|
|
28
29
|
import Clock from './clock';
|
|
30
|
+
import AddressEditor from './address';
|
|
29
31
|
|
|
30
32
|
const LocationIcon = lazy(() => import('@arcblock/icons/lib/Location'));
|
|
31
33
|
const TimezoneIcon = lazy(() => import('@arcblock/icons/lib/Timezone'));
|
|
@@ -69,7 +71,7 @@ export default function UserMetadataComponent({
|
|
|
69
71
|
isMobile: boolean;
|
|
70
72
|
isMyself: boolean;
|
|
71
73
|
user: User;
|
|
72
|
-
onSave: (v: UserMetadata) => void;
|
|
74
|
+
onSave: (v: { metadata: UserMetadata; address: UserAddress }) => void;
|
|
73
75
|
}) {
|
|
74
76
|
const [editable, setEditable] = useState(false);
|
|
75
77
|
const [visible, setVisible] = useState(false);
|
|
@@ -80,6 +82,13 @@ export default function UserMetadataComponent({
|
|
|
80
82
|
email: '',
|
|
81
83
|
phone: '',
|
|
82
84
|
});
|
|
85
|
+
const addressValidateMsg = useReactive<Record<string, string>>({
|
|
86
|
+
country: '',
|
|
87
|
+
province: '',
|
|
88
|
+
city: '',
|
|
89
|
+
detailedAddress: '',
|
|
90
|
+
postalCode: '',
|
|
91
|
+
});
|
|
83
92
|
|
|
84
93
|
useEffect(() => {
|
|
85
94
|
if (!isMobileView) {
|
|
@@ -113,6 +122,31 @@ export default function UserMetadataComponent({
|
|
|
113
122
|
}
|
|
114
123
|
);
|
|
115
124
|
|
|
125
|
+
const address = useCreation(() => {
|
|
126
|
+
return (
|
|
127
|
+
user?.address || {
|
|
128
|
+
country: '',
|
|
129
|
+
province: '',
|
|
130
|
+
city: '',
|
|
131
|
+
detailedAddress: '',
|
|
132
|
+
postalCode: '',
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}, [user?.address]);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 获取默认的国家
|
|
139
|
+
* 如果 address 中存储有 country 信息就使用 address 中的信息
|
|
140
|
+
* 如果没有根据 locale 获取,如果是中文环境,默认 country 是中国,如果是其他就选择美国
|
|
141
|
+
*/
|
|
142
|
+
const defaultCountry = useMemo(() => {
|
|
143
|
+
if (user?.address?.country) {
|
|
144
|
+
return user.address.country;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return locale === 'zh' ? 'China' : 'United States';
|
|
148
|
+
}, [user?.address?.country, locale]);
|
|
149
|
+
|
|
116
150
|
const phoneValue = useCreation((): PhoneValue => {
|
|
117
151
|
const phone = metadata.phone ??
|
|
118
152
|
user?.phone ?? {
|
|
@@ -143,6 +177,21 @@ export default function UserMetadataComponent({
|
|
|
143
177
|
metadata[field] = v;
|
|
144
178
|
};
|
|
145
179
|
|
|
180
|
+
const onAddressChange = (field: keyof UserAddress, value: string) => {
|
|
181
|
+
address[field] = value;
|
|
182
|
+
|
|
183
|
+
if (field === 'city') {
|
|
184
|
+
onChange(value, 'location');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (field === 'postalCode') {
|
|
188
|
+
addressValidateMsg.postalCode =
|
|
189
|
+
value && !isPostalCode(value, 'any') ? t('profile.address.invalidPostalCode') : '';
|
|
190
|
+
} else {
|
|
191
|
+
addressValidateMsg[field] = '';
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
146
195
|
const onEdit = () => {
|
|
147
196
|
if (!isMobileView) {
|
|
148
197
|
setEditable(true);
|
|
@@ -159,8 +208,22 @@ export default function UserMetadataComponent({
|
|
|
159
208
|
(metadata[k] as any) = defaultMetadata[k];
|
|
160
209
|
});
|
|
161
210
|
}
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
|
|
212
|
+
const defaultAddress: UserAddress = cloneDeep(user?.address) ?? {};
|
|
213
|
+
if (defaultAddress) {
|
|
214
|
+
Object.keys(address).forEach((key) => {
|
|
215
|
+
const k = key as keyof UserAddress;
|
|
216
|
+
(address[k] as any) = defaultAddress[k];
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 清空校验状态
|
|
221
|
+
[validateMsg, addressValidateMsg].forEach((o) => {
|
|
222
|
+
Object.keys(o).forEach((key) => {
|
|
223
|
+
o[key] = '';
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
164
227
|
if (!isMobileView) {
|
|
165
228
|
setEditable(false);
|
|
166
229
|
} else {
|
|
@@ -210,7 +273,17 @@ export default function UserMetadataComponent({
|
|
|
210
273
|
}
|
|
211
274
|
});
|
|
212
275
|
|
|
213
|
-
|
|
276
|
+
// 单独处理邮政编码验证,避免嵌套三元表达式
|
|
277
|
+
if (address.postalCode && !isPostalCode(address.postalCode, 'any')) {
|
|
278
|
+
addressValidateMsg.postalCode = t('profile.address.invalidPostalCode');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 检查是否有错误
|
|
282
|
+
if ([validateMsg, addressValidateMsg].some((o) => Object.values(o).some((e) => e))) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
onSave({ metadata, address });
|
|
214
287
|
setEditable(false);
|
|
215
288
|
setVisible(false);
|
|
216
289
|
};
|
|
@@ -253,14 +326,42 @@ export default function UserMetadataComponent({
|
|
|
253
326
|
{t('profile.editProfile')}
|
|
254
327
|
</Button>
|
|
255
328
|
) : null}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
|
|
330
|
+
{editing && isMyself ? (
|
|
331
|
+
<AddressEditor
|
|
332
|
+
address={address}
|
|
333
|
+
errors={addressValidateMsg}
|
|
334
|
+
handleChange={onAddressChange}
|
|
335
|
+
defaultCountry={defaultCountry}
|
|
336
|
+
/>
|
|
337
|
+
) : (
|
|
338
|
+
<EditableField
|
|
339
|
+
value={metadata.location ?? user?.address?.city ?? ''}
|
|
340
|
+
onChange={(value) => onChange(value, 'location')}
|
|
341
|
+
editable={editing}
|
|
342
|
+
placeholder="Location"
|
|
343
|
+
label={t('profile.location')}
|
|
344
|
+
tooltip={
|
|
345
|
+
isMyself ? (
|
|
346
|
+
<Box fontSize="14px">
|
|
347
|
+
<Typography variant="caption" component="p" fontWeight={600}>
|
|
348
|
+
{t('profile.address.detailedAddress')}
|
|
349
|
+
</Typography>
|
|
350
|
+
<Typography variant="caption" component="span">
|
|
351
|
+
{address.detailedAddress}
|
|
352
|
+
</Typography>
|
|
353
|
+
</Box>
|
|
354
|
+
) : null
|
|
355
|
+
}
|
|
356
|
+
renderValue={() => {
|
|
357
|
+
const fullLocation = [address.country, address.province, address.city || metadata.location || '']
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.join(' ');
|
|
360
|
+
return <Typography component="span">{fullLocation}</Typography>;
|
|
361
|
+
}}
|
|
362
|
+
icon={<LocationIcon {...iconSize} />}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
264
365
|
|
|
265
366
|
<EditableField
|
|
266
367
|
value={metadata.timezone || currentTimezone}
|
|
@@ -15,7 +15,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
|
15
15
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
16
16
|
|
|
17
17
|
import { translations } from '../../libs/locales';
|
|
18
|
-
import type { User, UserMetadata } from '../../../@types';
|
|
18
|
+
import type { User, UserAddress, UserMetadata } from '../../../@types';
|
|
19
19
|
import { formatAxiosError } from '../../libs/utils';
|
|
20
20
|
import { currentTimezone, getStatusDuration, isValidUrl } from './utils';
|
|
21
21
|
import SwitchRole from './switch-role';
|
|
@@ -32,6 +32,7 @@ export default function UserBasicInfo({
|
|
|
32
32
|
switchProfile,
|
|
33
33
|
isMobile = false,
|
|
34
34
|
onlyProfile = false,
|
|
35
|
+
refreshProfile,
|
|
35
36
|
...rest
|
|
36
37
|
}: {
|
|
37
38
|
user: User;
|
|
@@ -42,6 +43,7 @@ export default function UserBasicInfo({
|
|
|
42
43
|
size?: number;
|
|
43
44
|
isMobile?: boolean;
|
|
44
45
|
onlyProfile?: boolean;
|
|
46
|
+
refreshProfile: () => void;
|
|
45
47
|
} & BoxProps) {
|
|
46
48
|
const { locale } = useLocaleContext();
|
|
47
49
|
const [userStatus, setUserStatus] = useState<UserMetadata['status']>(undefined);
|
|
@@ -77,6 +79,7 @@ export default function UserBasicInfo({
|
|
|
77
79
|
status: v || {},
|
|
78
80
|
},
|
|
79
81
|
});
|
|
82
|
+
refreshProfile();
|
|
80
83
|
} catch (err) {
|
|
81
84
|
console.error(err);
|
|
82
85
|
Toast.error(formatAxiosError(err as AxiosError));
|
|
@@ -91,13 +94,14 @@ export default function UserBasicInfo({
|
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
const onSave = async (v: UserMetadata) => {
|
|
97
|
+
const onSave = async (v: { metadata: UserMetadata; address: UserAddress }) => {
|
|
95
98
|
if (!isMyself) {
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
101
|
+
const { metadata, address } = v;
|
|
98
102
|
try {
|
|
99
103
|
const newLinks =
|
|
100
|
-
|
|
104
|
+
metadata?.links
|
|
101
105
|
?.map((link) => {
|
|
102
106
|
if (!link.url || !isValidUrl(link.url)) return null;
|
|
103
107
|
|
|
@@ -114,10 +118,10 @@ export default function UserBasicInfo({
|
|
|
114
118
|
}
|
|
115
119
|
})
|
|
116
120
|
.filter((l) => !!l) || [];
|
|
117
|
-
|
|
118
|
-
// TODO: 需要更新 SDK
|
|
121
|
+
metadata.links = newLinks;
|
|
119
122
|
// @ts-ignore
|
|
120
|
-
await client.user.saveProfile({ metadata
|
|
123
|
+
await client.user.saveProfile({ metadata, address });
|
|
124
|
+
refreshProfile();
|
|
121
125
|
} catch (err) {
|
|
122
126
|
console.error(err);
|
|
123
127
|
Toast.error(formatAxiosError(err as AxiosError));
|
|
@@ -141,6 +141,15 @@ export const translations = {
|
|
|
141
141
|
afternoon: '下午',
|
|
142
142
|
night: '晚上',
|
|
143
143
|
},
|
|
144
|
+
address: {
|
|
145
|
+
title: '地址',
|
|
146
|
+
country: '国家/地区',
|
|
147
|
+
province: '州/省',
|
|
148
|
+
city: '城市/镇',
|
|
149
|
+
detailedAddress: '详细地址',
|
|
150
|
+
postalCode: '邮政编码',
|
|
151
|
+
invalidPostalCode: '邮政编码格式不正确',
|
|
152
|
+
},
|
|
144
153
|
},
|
|
145
154
|
},
|
|
146
155
|
en: {
|
|
@@ -286,6 +295,15 @@ export const translations = {
|
|
|
286
295
|
afternoon: 'PM',
|
|
287
296
|
night: 'PM',
|
|
288
297
|
},
|
|
298
|
+
address: {
|
|
299
|
+
title: 'Address',
|
|
300
|
+
country: 'Country/Region',
|
|
301
|
+
province: 'State/Province',
|
|
302
|
+
city: 'City/Town',
|
|
303
|
+
detailedAddress: 'Detailed Address',
|
|
304
|
+
postalCode: 'Postal Code',
|
|
305
|
+
invalidPostalCode: 'Postal code is invalid',
|
|
306
|
+
},
|
|
289
307
|
},
|
|
290
308
|
},
|
|
291
309
|
};
|