@shopbb/helium 0.6.3 → 0.7.0
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/dist/components/AddToCartButton.d.ts +17 -22
- package/dist/components/AddToCartButton.d.ts.map +1 -1
- package/dist/components/AddToCartButton.js +8 -45
- package/dist/components/AddToCartButton.js.map +1 -1
- package/dist/components/AddressForm.d.ts +42 -18
- package/dist/components/AddressForm.d.ts.map +1 -1
- package/dist/components/AddressForm.js +23 -20
- package/dist/components/AddressForm.js.map +1 -1
- package/dist/components/AddressList.d.ts +34 -17
- package/dist/components/AddressList.d.ts.map +1 -1
- package/dist/components/AddressList.js +7 -21
- package/dist/components/AddressList.js.map +1 -1
- package/dist/components/AddressPicker.d.ts +14 -16
- package/dist/components/AddressPicker.d.ts.map +1 -1
- package/dist/components/AddressPicker.js +10 -26
- package/dist/components/AddressPicker.js.map +1 -1
- package/dist/components/AnalyticsProvider.d.ts +5 -2
- package/dist/components/AnalyticsProvider.d.ts.map +1 -1
- package/dist/components/AnalyticsProvider.js +13 -11
- package/dist/components/AnalyticsProvider.js.map +1 -1
- package/dist/components/BuyNowButton.d.ts +7 -24
- package/dist/components/BuyNowButton.d.ts.map +1 -1
- package/dist/components/BuyNowButton.js +9 -43
- package/dist/components/BuyNowButton.js.map +1 -1
- package/dist/components/CartCheckoutButton.d.ts +10 -21
- package/dist/components/CartCheckoutButton.d.ts.map +1 -1
- package/dist/components/CartCheckoutButton.js +6 -11
- package/dist/components/CartCheckoutButton.js.map +1 -1
- package/dist/components/CartCost.d.ts +15 -23
- package/dist/components/CartCost.d.ts.map +1 -1
- package/dist/components/CartCost.js +1 -3
- package/dist/components/CartCost.js.map +1 -1
- package/dist/components/CartForm.d.ts +30 -102
- package/dist/components/CartForm.d.ts.map +1 -1
- package/dist/components/CartForm.js +32 -172
- package/dist/components/CartForm.js.map +1 -1
- package/dist/components/DiscountComponents.d.ts +67 -17
- package/dist/components/DiscountComponents.d.ts.map +1 -1
- package/dist/components/DiscountComponents.js +28 -74
- package/dist/components/DiscountComponents.js.map +1 -1
- package/dist/components/DiscountSelector.d.ts +50 -15
- package/dist/components/DiscountSelector.d.ts.map +1 -1
- package/dist/components/DiscountSelector.js +16 -44
- package/dist/components/DiscountSelector.js.map +1 -1
- package/dist/components/hooks/useOptimisticCart.d.ts +36 -37
- package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -1
- package/dist/components/hooks/useOptimisticCart.js +95 -127
- package/dist/components/hooks/useOptimisticCart.js.map +1 -1
- package/dist/components/index.d.ts +24 -45
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +21 -37
- package/dist/components/index.js.map +1 -1
- package/dist/createCartHandler.d.ts.map +1 -1
- package/dist/createCartHandler.js +8 -1
- package/dist/createCartHandler.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/src/components/AddToCartButton.tsx +34 -92
- package/src/components/AddressForm.tsx +56 -26
- package/src/components/AddressList.tsx +42 -33
- package/src/components/AddressPicker.tsx +19 -29
- package/src/components/AnalyticsProvider.tsx +18 -13
- package/src/components/BuyNowButton.tsx +28 -93
- package/src/components/CartCheckoutButton.tsx +16 -33
- package/src/components/CartCost.tsx +16 -28
- package/src/components/CartForm.tsx +87 -231
- package/src/components/DiscountComponents.tsx +94 -100
- package/src/components/DiscountSelector.tsx +68 -49
- package/src/components/hooks/useOptimisticCart.ts +122 -156
- package/src/components/index.ts +51 -99
- package/src/createCartHandler.ts +10 -1
- package/src/index.ts +0 -2
- /package/src/components/{AddressBookProvider.tsx → AddressBookProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/{CartLineQuantityAdjustButton.tsx → CartLineQuantityAdjustButton.tsx.deleted-0.7} +0 -0
- /package/src/components/{CartProvider.tsx → CartProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/{DiscountProvider.tsx → DiscountProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/hooks/{useMounted.ts → useMounted.ts.deleted-0.7} +0 -0
- /package/src/{handleCartFormAction.ts → handleCartFormAction.ts.deleted-0.7} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopbb/helium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,15 +49,8 @@
|
|
|
49
49
|
"repository": "https://github.com/shopbb/shopbb",
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": ">=18",
|
|
52
|
-
"react-dom": ">=18"
|
|
53
|
-
|
|
54
|
-
"peerDependenciesMeta": {
|
|
55
|
-
"react": {
|
|
56
|
-
"optional": true
|
|
57
|
-
},
|
|
58
|
-
"react-dom": {
|
|
59
|
-
"optional": true
|
|
60
|
-
}
|
|
52
|
+
"react-dom": ">=18",
|
|
53
|
+
"react-router": ">=7"
|
|
61
54
|
},
|
|
62
55
|
"devDependencies": {
|
|
63
56
|
"@cloudflare/workers-types": "^4.20240117.0",
|
|
@@ -65,6 +58,7 @@
|
|
|
65
58
|
"@types/react-dom": "^18",
|
|
66
59
|
"react": "^18.3.0",
|
|
67
60
|
"react-dom": "^18.3.0",
|
|
61
|
+
"react-router": "^7.0.0",
|
|
68
62
|
"typescript": "^5.3.3"
|
|
69
63
|
},
|
|
70
64
|
"engines": {
|
|
@@ -1,47 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <AddToCartButton> — 加入购物车按钮
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* helium 0.7:对齐 Hydrogen 模式。这是 <CartForm action=LinesAdd> 的便捷包装。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* - hydrate 后 JS 拦截 submit → fetch /cart → CartProvider.applyCart → 无刷新
|
|
8
|
-
* - loading 状态由 CartForm.fetcher 暴露,通过 useFetcher() 读取
|
|
9
|
-
*
|
|
10
|
-
* 商家也可以直接组合 <CartForm>,不用我们这个 wrapper:
|
|
6
|
+
* 商家也可以直接用 <CartForm>(更灵活):
|
|
11
7
|
*
|
|
12
8
|
* <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines: [...] }}>
|
|
13
|
-
*
|
|
9
|
+
* {(fetcher) => (
|
|
10
|
+
* <button type="submit" disabled={fetcher.state !== 'idle'}>
|
|
11
|
+
* {fetcher.state === 'idle' ? '加入购物车' : '加入中...'}
|
|
12
|
+
* </button>
|
|
13
|
+
* )}
|
|
14
14
|
* </CartForm>
|
|
15
|
+
*
|
|
16
|
+
* 想要加购后自定义行为(弹抽屉、跳页等):商家组件用 useFetcher / useNavigation
|
|
17
|
+
* 监听全局 fetcher 状态。Hydrogen demo store 用 `useAside().open()` 模式。
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import * as React from 'react';
|
|
18
|
-
import { CartForm,
|
|
19
|
-
import { useAnalytics } from './AnalyticsProvider';
|
|
21
|
+
import { CartForm, type CartLineInput } from './CartForm';
|
|
20
22
|
|
|
21
23
|
export interface AddToCartButtonProps {
|
|
22
|
-
/**
|
|
24
|
+
/** variant GID */
|
|
23
25
|
variantId: string;
|
|
24
26
|
/** 数量,默认 1 */
|
|
25
27
|
quantity?: number;
|
|
26
|
-
/** 行级 attributes
|
|
28
|
+
/** 行级 attributes */
|
|
27
29
|
attributes?: Array<{ key: string; value: string }>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
onError?: (err: Error) => void;
|
|
32
|
-
/** 加购中显示的文本 */
|
|
33
|
-
loadingText?: React.ReactNode;
|
|
34
|
-
/** 不可用时显示的文本 */
|
|
30
|
+
/** optimistic UI 用:传整个 variant 对象(含 product 等) */
|
|
31
|
+
selectedVariant?: any;
|
|
32
|
+
/** Disabled 文本 */
|
|
35
33
|
unavailableText?: React.ReactNode;
|
|
36
|
-
/** disabled */
|
|
37
34
|
disabled?: boolean;
|
|
38
|
-
/** 包装 form className */
|
|
39
35
|
className?: string;
|
|
40
|
-
/**
|
|
41
|
-
buttonClassName?: string;
|
|
42
|
-
/** 提交的 route,默认 /cart */
|
|
36
|
+
/** 提交 route,默认 /cart */
|
|
43
37
|
route?: string;
|
|
44
|
-
/**
|
|
38
|
+
/** 按钮 children */
|
|
45
39
|
children?: React.ReactNode;
|
|
46
40
|
}
|
|
47
41
|
|
|
@@ -50,90 +44,38 @@ export function AddToCartButton(props: AddToCartButtonProps) {
|
|
|
50
44
|
variantId,
|
|
51
45
|
quantity = 1,
|
|
52
46
|
attributes,
|
|
53
|
-
|
|
54
|
-
onError,
|
|
55
|
-
loadingText = '加入中...',
|
|
47
|
+
selectedVariant,
|
|
56
48
|
unavailableText = '缺货',
|
|
57
49
|
disabled = false,
|
|
58
50
|
className,
|
|
59
|
-
buttonClassName,
|
|
60
51
|
route = '/cart',
|
|
61
52
|
children = '加入购物车',
|
|
62
53
|
} = props;
|
|
63
54
|
|
|
64
55
|
const line: CartLineInput = { merchandiseId: variantId, quantity };
|
|
65
56
|
if (attributes && attributes.length > 0) line.attributes = attributes;
|
|
57
|
+
if (selectedVariant) line.selectedVariant = selectedVariant;
|
|
66
58
|
|
|
67
59
|
return (
|
|
68
60
|
<CartForm
|
|
69
61
|
route={route}
|
|
70
62
|
action={CartForm.ACTIONS.LinesAdd}
|
|
71
63
|
inputs={{ lines: [line] }}
|
|
72
|
-
className={className}
|
|
73
64
|
>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
65
|
+
{(fetcher) => {
|
|
66
|
+
const submitting = fetcher.state !== 'idle';
|
|
67
|
+
return (
|
|
68
|
+
<button
|
|
69
|
+
type="submit"
|
|
70
|
+
className={className}
|
|
71
|
+
disabled={disabled || submitting}
|
|
72
|
+
data-add-to-cart
|
|
73
|
+
data-loading={submitting ? '' : undefined}
|
|
74
|
+
>
|
|
75
|
+
{submitting ? '加入中...' : disabled ? unavailableText : children}
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
}}
|
|
86
79
|
</CartForm>
|
|
87
80
|
);
|
|
88
81
|
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Inner component — 合法地 useFetcher / useEffect,监听 fetcher.state 变化。
|
|
92
|
-
*/
|
|
93
|
-
function AddToCartButtonInner({
|
|
94
|
-
disabled, buttonClassName, loadingText, unavailableText, children,
|
|
95
|
-
variantId, quantity, onAdded, onError,
|
|
96
|
-
}: {
|
|
97
|
-
disabled: boolean;
|
|
98
|
-
buttonClassName?: string;
|
|
99
|
-
loadingText: React.ReactNode;
|
|
100
|
-
unavailableText: React.ReactNode;
|
|
101
|
-
children: React.ReactNode;
|
|
102
|
-
variantId: string;
|
|
103
|
-
quantity: number;
|
|
104
|
-
onAdded?: () => void;
|
|
105
|
-
onError?: (err: Error) => void;
|
|
106
|
-
}) {
|
|
107
|
-
const fetcher = useFetcher();
|
|
108
|
-
const analytics = useAnalytics();
|
|
109
|
-
|
|
110
|
-
// 监听 fetcher 完成
|
|
111
|
-
const prevState = React.useRef(fetcher.state);
|
|
112
|
-
React.useEffect(() => {
|
|
113
|
-
const wasNonIdle = prevState.current !== 'idle';
|
|
114
|
-
const isIdle = fetcher.state === 'idle';
|
|
115
|
-
if (wasNonIdle && isIdle) {
|
|
116
|
-
if (fetcher.error) {
|
|
117
|
-
onError?.(new Error(fetcher.error));
|
|
118
|
-
} else if (fetcher.data?.cart) {
|
|
119
|
-
analytics.emit('add_to_cart', { variantId, quantity });
|
|
120
|
-
onAdded?.();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
prevState.current = fetcher.state;
|
|
124
|
-
}, [fetcher.state, fetcher.error, fetcher.data, onAdded, onError, analytics, variantId, quantity]);
|
|
125
|
-
|
|
126
|
-
const adding = fetcher.state !== 'idle';
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<button
|
|
130
|
-
type="submit"
|
|
131
|
-
className={buttonClassName}
|
|
132
|
-
disabled={disabled || adding}
|
|
133
|
-
data-add-to-cart
|
|
134
|
-
data-loading={adding ? '' : undefined}
|
|
135
|
-
>
|
|
136
|
-
{adding ? loadingText : disabled ? unavailableText : children}
|
|
137
|
-
</button>
|
|
138
|
-
);
|
|
139
|
-
}
|
|
@@ -1,41 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <AddressForm>
|
|
3
3
|
*
|
|
4
|
-
* 地址编辑 /
|
|
5
|
-
* - 表单 state(useState)
|
|
6
|
-
* - 必填验证
|
|
7
|
-
* - 调 useAddressBook().createAddress / updateAddress
|
|
8
|
-
* - userErrors 显示
|
|
9
|
-
* - loading 态
|
|
4
|
+
* 地址编辑 / 新增表单。
|
|
10
5
|
*
|
|
11
|
-
*
|
|
12
|
-
* <AddressForm onSave={() => setCreating(false)} onCancel={...} />
|
|
6
|
+
* helium 0.7:删除 useAddressBook 调用,改为接 onSubmit prop(商家自己处理 mutation)。
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
* <AddressForm
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* <AddressForm
|
|
10
|
+
* initial={editingAddress}
|
|
11
|
+
* onSubmit={async (input, asDefault) => {
|
|
12
|
+
* // 商家调自己的 action 或 fetch
|
|
13
|
+
* const result = await myCreateOrUpdate(input, asDefault);
|
|
14
|
+
* return { userErrors: result.userErrors, address: result.address };
|
|
15
|
+
* }}
|
|
16
|
+
* onSaved={(addr) => setCreating(false)}
|
|
17
|
+
* onCancel={() => setCreating(false)}
|
|
18
|
+
* />
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import * as React from 'react';
|
|
21
|
-
import {
|
|
22
|
+
import type { Address } from './AddressList';
|
|
23
|
+
|
|
24
|
+
export interface AddressInput {
|
|
25
|
+
firstName: string;
|
|
26
|
+
lastName?: string;
|
|
27
|
+
company?: string;
|
|
28
|
+
phone: string;
|
|
29
|
+
country?: string;
|
|
30
|
+
countryCode?: string;
|
|
31
|
+
province: string;
|
|
32
|
+
provinceCode?: string;
|
|
33
|
+
city: string;
|
|
34
|
+
district?: string;
|
|
35
|
+
address1: string;
|
|
36
|
+
address2?: string;
|
|
37
|
+
zip?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AddressBookUserError {
|
|
41
|
+
field?: string[];
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AddressFormSubmitResult {
|
|
47
|
+
address?: Address;
|
|
48
|
+
userErrors: AddressBookUserError[];
|
|
49
|
+
}
|
|
22
50
|
|
|
23
51
|
export interface AddressFormProps {
|
|
24
52
|
/** 传 = 编辑模式;不传 = 新增模式 */
|
|
25
53
|
initial?: Address;
|
|
54
|
+
/** 提交回调(商家实现 mutation)—— 必填 */
|
|
55
|
+
onSubmit?: (input: AddressInput, asDefault: boolean) => Promise<AddressFormSubmitResult>;
|
|
26
56
|
/** 保存成功后触发 */
|
|
27
|
-
|
|
57
|
+
onSaved?: (address?: Address) => void;
|
|
28
58
|
/** 取消按钮触发 */
|
|
29
59
|
onCancel?: () => void;
|
|
30
|
-
/** 自定义提交按钮文字 */
|
|
31
60
|
submitText?: string;
|
|
32
61
|
cancelText?: string;
|
|
33
|
-
/** 显示"设为默认"
|
|
62
|
+
/** 显示"设为默认"复选框 */
|
|
34
63
|
showDefaultCheckbox?: boolean;
|
|
35
|
-
/** 哪些字段必填,默认 ['firstName','phone','address1','city','province'] */
|
|
36
64
|
requiredFields?: Array<keyof AddressInput>;
|
|
37
65
|
className?: string;
|
|
38
|
-
/** i18n 文案覆盖 */
|
|
39
66
|
i18n?: Partial<AddressFormI18n>;
|
|
40
67
|
}
|
|
41
68
|
|
|
@@ -73,7 +100,7 @@ const DEFAULT_I18N: AddressFormI18n = {
|
|
|
73
100
|
|
|
74
101
|
export function AddressForm(props: AddressFormProps) {
|
|
75
102
|
const {
|
|
76
|
-
initial,
|
|
103
|
+
initial, onSubmit, onSaved, onCancel,
|
|
77
104
|
submitText = '保存',
|
|
78
105
|
cancelText = '取消',
|
|
79
106
|
showDefaultCheckbox = true,
|
|
@@ -82,7 +109,6 @@ export function AddressForm(props: AddressFormProps) {
|
|
|
82
109
|
i18n: i18nOverride,
|
|
83
110
|
} = props;
|
|
84
111
|
|
|
85
|
-
const { createAddress, updateAddress } = useAddressBook();
|
|
86
112
|
const i18n = { ...DEFAULT_I18N, ...i18nOverride };
|
|
87
113
|
|
|
88
114
|
const [form, setForm] = React.useState<AddressInput>({
|
|
@@ -104,27 +130,31 @@ export function AddressForm(props: AddressFormProps) {
|
|
|
104
130
|
const [saving, setSaving] = React.useState(false);
|
|
105
131
|
const [errors, setErrors] = React.useState<AddressBookUserError[]>([]);
|
|
106
132
|
|
|
107
|
-
const update = (k: keyof AddressInput, v: string) =>
|
|
133
|
+
const update = (k: keyof AddressInput, v: string) =>
|
|
134
|
+
setForm((f) => ({ ...f, [k]: v } as any));
|
|
108
135
|
|
|
109
136
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
110
137
|
e.preventDefault();
|
|
111
|
-
// 客户端必填校验
|
|
112
138
|
const missing = requiredFields.filter((k) => !(form[k] && String(form[k]).trim()));
|
|
113
139
|
if (missing.length > 0) {
|
|
114
140
|
setErrors([{ field: missing as string[], code: 'MISSING_FIELD', message: '请填写完整地址' }]);
|
|
115
141
|
return;
|
|
116
142
|
}
|
|
143
|
+
if (!onSubmit) {
|
|
144
|
+
setErrors([{ code: 'NO_HANDLER', message: '商家未传入 onSubmit 处理函数' }]);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
117
147
|
setSaving(true);
|
|
118
148
|
setErrors([]);
|
|
119
149
|
try {
|
|
120
|
-
const result =
|
|
121
|
-
? await updateAddress(initial.id, form, asDefault)
|
|
122
|
-
: await createAddress(form, asDefault);
|
|
150
|
+
const result = await onSubmit(form, asDefault);
|
|
123
151
|
if (result.userErrors.length > 0) {
|
|
124
152
|
setErrors(result.userErrors);
|
|
125
153
|
return;
|
|
126
154
|
}
|
|
127
|
-
|
|
155
|
+
onSaved?.(result.address);
|
|
156
|
+
} catch (e: any) {
|
|
157
|
+
setErrors([{ code: 'SUBMIT_ERROR', message: e?.message || '提交失败' }]);
|
|
128
158
|
} finally {
|
|
129
159
|
setSaving(false);
|
|
130
160
|
}
|
|
@@ -3,53 +3,63 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 渲染当前买家的地址列表。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* onAdd={() => openCreateModal()}
|
|
10
|
-
* emptyText="还没有保存的地址"
|
|
11
|
-
* />
|
|
6
|
+
* helium 0.7:addresses 通过 props 传入(不再用 AddressBookProvider)。
|
|
7
|
+
* 商家从 customer GraphQL 拉 addresses + defaultAddress 传给本组件。
|
|
8
|
+
* setDefault / remove 由商家通过 RR7 action 实现(onSetDefault / onRemove 回调)。
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import * as React from 'react';
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
|
|
13
|
+
export interface Address {
|
|
14
|
+
id: string;
|
|
15
|
+
firstName?: string | null;
|
|
16
|
+
lastName?: string | null;
|
|
17
|
+
name?: string | null;
|
|
18
|
+
company?: string | null;
|
|
19
|
+
phone?: string | null;
|
|
20
|
+
address1?: string | null;
|
|
21
|
+
address2?: string | null;
|
|
22
|
+
city?: string | null;
|
|
23
|
+
province?: string | null;
|
|
24
|
+
provinceCode?: string | null;
|
|
25
|
+
district?: string | null;
|
|
26
|
+
country?: string | null;
|
|
27
|
+
countryCode?: string | null;
|
|
28
|
+
zip?: string | null;
|
|
29
|
+
isDefault?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AddressListItemActions {
|
|
33
|
+
setDefault: () => Promise<void> | void;
|
|
34
|
+
remove: () => Promise<void> | void;
|
|
35
|
+
edit?: () => void;
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
export interface AddressListProps {
|
|
39
|
+
/** 地址列表(从 loader 传入) */
|
|
40
|
+
addresses?: Address[];
|
|
41
|
+
/** 设为默认地址回调 */
|
|
42
|
+
onSetDefault?: (addressId: string) => Promise<void> | void;
|
|
43
|
+
/** 删除地址回调 */
|
|
44
|
+
onRemove?: (addressId: string) => Promise<void> | void;
|
|
19
45
|
/** 编辑按钮点击 */
|
|
20
46
|
onEdit?: (address: Address) => void;
|
|
21
|
-
/**
|
|
47
|
+
/** 新增按钮点击 */
|
|
22
48
|
onAdd?: () => void;
|
|
23
49
|
/** 空状态文案 */
|
|
24
50
|
emptyText?: React.ReactNode;
|
|
25
|
-
/**
|
|
51
|
+
/** 渲染单个地址卡片的覆盖 */
|
|
26
52
|
renderItem?: (address: Address, actions: AddressListItemActions) => React.ReactNode;
|
|
27
|
-
/** 整体容器 className */
|
|
28
53
|
className?: string;
|
|
29
|
-
/** 加载中渲染 */
|
|
30
|
-
loadingFallback?: React.ReactNode;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface AddressListItemActions {
|
|
34
|
-
setDefault: () => Promise<void>;
|
|
35
|
-
remove: () => Promise<void>;
|
|
36
|
-
edit?: () => void;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
export function AddressList(props: AddressListProps) {
|
|
40
57
|
const {
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
addresses = [],
|
|
59
|
+
onSetDefault, onRemove, onEdit, onAdd,
|
|
60
|
+
emptyText = '还没有保存的地址',
|
|
61
|
+
renderItem, className,
|
|
43
62
|
} = props;
|
|
44
|
-
const mounted = useMounted();
|
|
45
|
-
const { addresses, status, setDefault, deleteAddress } = useAddressBook();
|
|
46
|
-
|
|
47
|
-
if (!mounted) {
|
|
48
|
-
return <div data-address-list data-ssr-placeholder className={className} />;
|
|
49
|
-
}
|
|
50
|
-
if (status === 'loading' || status === 'unauthenticated') {
|
|
51
|
-
return <>{loadingFallback}</>;
|
|
52
|
-
}
|
|
53
63
|
|
|
54
64
|
if (addresses.length === 0) {
|
|
55
65
|
return (
|
|
@@ -75,8 +85,8 @@ export function AddressList(props: AddressListProps) {
|
|
|
75
85
|
<div data-items>
|
|
76
86
|
{addresses.map((a) => {
|
|
77
87
|
const actions: AddressListItemActions = {
|
|
78
|
-
setDefault:
|
|
79
|
-
remove:
|
|
88
|
+
setDefault: () => onSetDefault?.(a.id) as any,
|
|
89
|
+
remove: () => onRemove?.(a.id) as any,
|
|
80
90
|
edit: onEdit ? () => onEdit(a) : undefined,
|
|
81
91
|
};
|
|
82
92
|
if (renderItem) return <React.Fragment key={a.id}>{renderItem(a, actions)}</React.Fragment>;
|
|
@@ -87,7 +97,6 @@ export function AddressList(props: AddressListProps) {
|
|
|
87
97
|
);
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
// 默认渲染
|
|
91
100
|
function DefaultAddressItem({ address: a, actions }: { address: Address; actions: AddressListItemActions }) {
|
|
92
101
|
return (
|
|
93
102
|
<div data-address-item data-default={a.isDefault ? '' : undefined}>
|
|
@@ -1,55 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <AddressPicker>
|
|
3
3
|
*
|
|
4
|
-
* Checkout
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - 选 "新地址" 时展开新增表单
|
|
4
|
+
* Checkout 用的地址选择器。
|
|
5
|
+
*
|
|
6
|
+
* helium 0.7:addresses 通过 props 传入。
|
|
8
7
|
*
|
|
9
|
-
* 用法(受控):
|
|
10
8
|
* <AddressPicker
|
|
9
|
+
* addresses={addresses}
|
|
10
|
+
* defaultAddress={defaultAddress}
|
|
11
11
|
* value={selectedAddressId}
|
|
12
12
|
* onChange={(id, addr) => setSelectedAddressId(id)}
|
|
13
|
-
* allowNewAddress
|
|
14
|
-
* onUseNewAddress={(addr) => setInlineAddress(addr)}
|
|
15
13
|
* />
|
|
16
|
-
*
|
|
17
|
-
* 用法(非受控):
|
|
18
|
-
* <AddressPicker onSelect={(addr) => ...} />
|
|
19
|
-
* 组件内部 state;选择后回调商家拿值。
|
|
20
14
|
*/
|
|
21
15
|
|
|
22
16
|
import * as React from 'react';
|
|
23
|
-
import {
|
|
24
|
-
import { useMounted } from './hooks/useMounted';
|
|
17
|
+
import type { Address } from './AddressList';
|
|
25
18
|
import { AddressForm } from './AddressForm';
|
|
26
19
|
|
|
27
20
|
export interface AddressPickerProps {
|
|
21
|
+
/** 地址列表(从 loader 传入) */
|
|
22
|
+
addresses?: Address[];
|
|
23
|
+
/** 默认地址(用来自动选中) */
|
|
24
|
+
defaultAddress?: Address | null;
|
|
28
25
|
/** 受控:当前选中的 address ID */
|
|
29
26
|
value?: string | null;
|
|
30
|
-
/**
|
|
27
|
+
/** 受控:选中变化 */
|
|
31
28
|
onChange?: (addressId: string | null, address: Address | null) => void;
|
|
32
|
-
/**
|
|
29
|
+
/** 非受控:选择时回调 */
|
|
33
30
|
onSelect?: (address: Address | null) => void;
|
|
34
31
|
/** 是否允许"使用新地址"选项 */
|
|
35
32
|
allowNewAddress?: boolean;
|
|
36
|
-
/** 选了"新地址"
|
|
33
|
+
/** 选了"新地址"后保存的回调 */
|
|
37
34
|
onUseNewAddress?: (address: Address) => void;
|
|
38
|
-
/**
|
|
35
|
+
/** 空状态 */
|
|
39
36
|
emptyFallback?: React.ReactNode;
|
|
40
37
|
className?: string;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
export function AddressPicker(props: AddressPickerProps) {
|
|
44
41
|
const {
|
|
42
|
+
addresses = [],
|
|
43
|
+
defaultAddress = null,
|
|
45
44
|
value, onChange, onSelect,
|
|
46
45
|
allowNewAddress = true,
|
|
47
46
|
onUseNewAddress,
|
|
48
47
|
emptyFallback,
|
|
49
48
|
className,
|
|
50
49
|
} = props;
|
|
51
|
-
const mounted = useMounted();
|
|
52
|
-
const { addresses, defaultAddress, status } = useAddressBook();
|
|
53
50
|
|
|
54
51
|
// 非受控 fallback
|
|
55
52
|
const [internalId, setInternalId] = React.useState<string | null>(null);
|
|
@@ -71,13 +68,6 @@ export function AddressPicker(props: AddressPickerProps) {
|
|
|
71
68
|
}
|
|
72
69
|
}, [defaultAddress, addresses, selectedId, setSelectedId]);
|
|
73
70
|
|
|
74
|
-
if (!mounted) {
|
|
75
|
-
return <div data-address-picker data-ssr-placeholder className={className} />;
|
|
76
|
-
}
|
|
77
|
-
if (status === 'loading') {
|
|
78
|
-
return <div data-address-picker data-loading>加载地址中…</div>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
71
|
// 0 个地址:直接展示一个新增表单
|
|
82
72
|
if (addresses.length === 0) {
|
|
83
73
|
if (emptyFallback) return <>{emptyFallback}</>;
|
|
@@ -85,7 +75,7 @@ export function AddressPicker(props: AddressPickerProps) {
|
|
|
85
75
|
<div data-address-picker data-empty className={className}>
|
|
86
76
|
<div data-picker-title>填写收货地址</div>
|
|
87
77
|
<AddressForm
|
|
88
|
-
|
|
78
|
+
onSaved={(addr) => {
|
|
89
79
|
if (addr) {
|
|
90
80
|
setSelectedId(addr.id, addr);
|
|
91
81
|
onUseNewAddress?.(addr);
|
|
@@ -129,7 +119,7 @@ export function AddressPicker(props: AddressPickerProps) {
|
|
|
129
119
|
/>
|
|
130
120
|
<div>
|
|
131
121
|
<div data-name>+ 使用新地址</div>
|
|
132
|
-
<div data-line
|
|
122
|
+
<div data-line>填写一个新地址</div>
|
|
133
123
|
</div>
|
|
134
124
|
</label>
|
|
135
125
|
)}
|
|
@@ -142,7 +132,7 @@ export function AddressPicker(props: AddressPickerProps) {
|
|
|
142
132
|
const fallback = defaultAddress || addresses[0];
|
|
143
133
|
if (fallback) setSelectedId(fallback.id, fallback);
|
|
144
134
|
}}
|
|
145
|
-
|
|
135
|
+
onSaved={(addr) => {
|
|
146
136
|
if (addr) {
|
|
147
137
|
setShowNewForm(false);
|
|
148
138
|
setSelectedId(addr.id, addr);
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import * as React from 'react';
|
|
30
|
-
import { useCartOptional } from './CartProvider';
|
|
31
30
|
import { useShopOptional } from './ShopProvider';
|
|
32
31
|
|
|
33
32
|
export interface AnalyticsEvent {
|
|
@@ -51,14 +50,16 @@ export interface AnalyticsProviderProps {
|
|
|
51
50
|
children: React.ReactNode;
|
|
52
51
|
/** 接收所有事件的回调 */
|
|
53
52
|
onEvent?: (event: AnalyticsEvent) => void;
|
|
54
|
-
/**
|
|
55
|
-
|
|
53
|
+
/**
|
|
54
|
+
* 商家可以传入当前 cart(从 loader 拿)。
|
|
55
|
+
* 每次 cart 变化(id 或 totalQuantity 变)自动触发 cart_updated 事件。
|
|
56
|
+
*/
|
|
57
|
+
cart?: any;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function AnalyticsProvider(props: AnalyticsProviderProps) {
|
|
59
|
-
const { children, onEvent,
|
|
61
|
+
const { children, onEvent, cart } = props;
|
|
60
62
|
const shop = useShopOptional();
|
|
61
|
-
const cartCtx = useCartOptional();
|
|
62
63
|
const onEventRef = React.useRef(onEvent);
|
|
63
64
|
React.useEffect(() => {
|
|
64
65
|
onEventRef.current = onEvent;
|
|
@@ -76,17 +77,21 @@ export function AnalyticsProvider(props: AnalyticsProviderProps) {
|
|
|
76
77
|
[shop],
|
|
77
78
|
);
|
|
78
79
|
|
|
79
|
-
// cart
|
|
80
|
+
// cart 自动追踪:通过 useEffect 比较 cart.id + totalQuantity 变化触发事件
|
|
81
|
+
const lastCartRef = React.useRef<{ id?: string; qty?: number } | null>(null);
|
|
80
82
|
React.useEffect(() => {
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
+
if (!cart) return;
|
|
84
|
+
const cur = { id: cart.id as string | undefined, qty: cart.totalQuantity as number | undefined };
|
|
85
|
+
const prev = lastCartRef.current;
|
|
86
|
+
if (!prev || prev.id !== cur.id || prev.qty !== cur.qty) {
|
|
87
|
+
lastCartRef.current = cur;
|
|
83
88
|
emit('cart_updated', {
|
|
84
|
-
cartId:
|
|
85
|
-
totalQuantity:
|
|
86
|
-
totalAmount:
|
|
89
|
+
cartId: cart.id,
|
|
90
|
+
totalQuantity: cart.totalQuantity,
|
|
91
|
+
totalAmount: cart.cost?.totalAmount,
|
|
87
92
|
});
|
|
88
|
-
}
|
|
89
|
-
}, [
|
|
93
|
+
}
|
|
94
|
+
}, [cart, emit]);
|
|
90
95
|
|
|
91
96
|
return <Ctx.Provider value={{ emit }}>{children}</Ctx.Provider>;
|
|
92
97
|
}
|