@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.
@@ -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(Tooltip, { title: tooltip, placement: "top", children: /* @__PURE__ */ jsxs(
135
- Typography,
145
+ return value ? /* @__PURE__ */ jsx(
146
+ Tooltip,
136
147
  {
137
- variant: "subtitle1",
138
- component: "div",
139
- gutterBottom: true,
140
- sx: {
141
- width: "100%",
142
- mb: 0,
143
- lineHeight: 1.4,
144
- overflow: "hidden"
145
- },
146
- display: "flex",
147
- alignItems: "center",
148
- gap: 1,
149
- children: [
150
- icon,
151
- /* @__PURE__ */ jsxs(Box, { display: "flex", flexDirection: "row", alignItems: "center", width: "90%", children: [
152
- /* @__PURE__ */ jsx(
153
- Typography,
154
- {
155
- sx: {
156
- whiteSpace: "pre-wrap",
157
- ...inline ? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } : {}
158
- },
159
- children: renderValue ? renderValue(value) : value
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
- verified && /* @__PURE__ */ jsx(VerifiedIcon, { color: "success", style: { fontSize: 16, width: 16, marginLeft: 4, flexShrink: 0 } })
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
- ) }) : null;
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: "h6", sx: { fontSize: "16px !important", mb: 0 }, children: t("profile.setStatus") }),
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: UserMetadata) => void;
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
- validateMsg.email = "";
134
- validateMsg.phone = "";
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
- onSave(metadata);
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 = v?.links?.map((link) => {
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
- v.links = newLinks;
93
- await client.user.saveProfile({ metadata: v });
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.28",
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.28",
37
- "@arcblock/react-hooks": "^2.12.28",
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": "f5d659cb0c3bf7d4b94202c17a603c861aed3f8d"
90
+ "gitHead": "97305a61162566787fbc38decf9d26dcdb556637"
91
91
  }
@@ -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
- <Typography
133
- position="absolute"
134
- bottom={-22}
135
- right={2}
136
- variant="caption"
137
- fontSize="12px"
138
- component="span"
139
- color={isTooLong ? 'error' : 'text.secondary'}>
140
- {isTooLong ? `(${t('profile.maxInputLength')} ${maxLength}) ` : ''}
141
- {value.length} / {maxLength}
142
- </Typography>
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 title={tooltip} placement="top">
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="h6" sx={{ fontSize: '16px !important', mb: 0 }}>
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
- validateMsg.email = '';
163
- validateMsg.phone = '';
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
- onSave(metadata);
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
- <EditableField
257
- value={metadata.location ?? ''}
258
- onChange={(value) => onChange(value, 'location')}
259
- editable={editing}
260
- placeholder="Location"
261
- label={t('profile.location')}
262
- icon={<LocationIcon {...iconSize} />}
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
- v?.links
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
- v.links = newLinks;
118
- // TODO: 需要更新 SDK
121
+ metadata.links = newLinks;
119
122
  // @ts-ignore
120
- await client.user.saveProfile({ metadata: v });
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
  };