@shopbb/helium 0.5.10 → 0.6.1
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/cache/withCache.d.ts +49 -0
- package/dist/cache/withCache.d.ts.map +1 -0
- package/dist/cache/withCache.js +117 -0
- package/dist/cache/withCache.js.map +1 -0
- package/dist/components/AddToCartButton.d.ts +28 -22
- package/dist/components/AddToCartButton.d.ts.map +1 -1
- package/dist/components/AddToCartButton.js +36 -47
- package/dist/components/AddToCartButton.js.map +1 -1
- package/dist/components/BuyNowButton.d.ts +45 -0
- package/dist/components/BuyNowButton.d.ts.map +1 -0
- package/dist/components/BuyNowButton.js +49 -0
- package/dist/components/BuyNowButton.js.map +1 -0
- package/dist/components/CartCheckoutButton.d.ts +39 -0
- package/dist/components/CartCheckoutButton.d.ts.map +1 -0
- package/dist/components/CartCheckoutButton.js +32 -0
- package/dist/components/CartCheckoutButton.js.map +1 -0
- package/dist/components/CartCost.d.ts +43 -0
- package/dist/components/CartCost.d.ts.map +1 -0
- package/dist/components/CartCost.js +34 -0
- package/dist/components/CartCost.js.map +1 -0
- package/dist/components/CartForm.d.ts +201 -0
- package/dist/components/CartForm.d.ts.map +1 -0
- package/dist/components/CartForm.js +213 -0
- package/dist/components/CartForm.js.map +1 -0
- package/dist/components/CartLineProvider.d.ts +78 -0
- package/dist/components/CartLineProvider.d.ts.map +1 -0
- package/dist/components/CartLineProvider.js +46 -0
- package/dist/components/CartLineProvider.js.map +1 -0
- package/dist/components/CartLineQuantity.d.ts +24 -0
- package/dist/components/CartLineQuantity.d.ts.map +1 -0
- package/dist/components/CartLineQuantity.js +9 -0
- package/dist/components/CartLineQuantity.js.map +1 -0
- package/dist/components/DiscountSelector.d.ts.map +1 -1
- package/dist/components/DiscountSelector.js +8 -19
- package/dist/components/DiscountSelector.js.map +1 -1
- package/dist/components/Image.d.ts +18 -0
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +26 -0
- package/dist/components/Image.js.map +1 -1
- package/dist/components/Pagination.d.ts +82 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +84 -0
- package/dist/components/Pagination.js.map +1 -0
- package/dist/components/RichText.d.ts +78 -0
- package/dist/components/RichText.d.ts.map +1 -0
- package/dist/components/RichText.js +93 -0
- package/dist/components/RichText.js.map +1 -0
- package/dist/components/Seo.d.ts +25 -0
- package/dist/components/Seo.d.ts.map +1 -0
- package/dist/components/Seo.js +54 -0
- package/dist/components/Seo.js.map +1 -0
- package/dist/components/hooks/useMoney.d.ts +40 -0
- package/dist/components/hooks/useMoney.d.ts.map +1 -0
- package/dist/components/hooks/useMoney.js +60 -0
- package/dist/components/hooks/useMoney.js.map +1 -0
- package/dist/components/hooks/useOptimisticCart.d.ts +50 -0
- package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -0
- package/dist/components/hooks/useOptimisticCart.js +146 -0
- package/dist/components/hooks/useOptimisticCart.js.map +1 -0
- package/dist/components/index.d.ts +28 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +21 -0
- package/dist/components/index.js.map +1 -1
- package/dist/createCartHandler.d.ts.map +1 -1
- package/dist/createCartHandler.js +57 -0
- package/dist/createCartHandler.js.map +1 -1
- package/dist/csp/csp.d.ts +57 -0
- package/dist/csp/csp.d.ts.map +1 -0
- package/dist/csp/csp.js +73 -0
- package/dist/csp/csp.js.map +1 -0
- package/dist/customer/createCustomerAccountClient.d.ts +43 -0
- package/dist/customer/createCustomerAccountClient.d.ts.map +1 -0
- package/dist/customer/createCustomerAccountClient.js +68 -0
- package/dist/customer/createCustomerAccountClient.js.map +1 -0
- package/dist/handleCartFormAction.d.ts +39 -0
- package/dist/handleCartFormAction.d.ts.map +1 -0
- package/dist/handleCartFormAction.js +103 -0
- package/dist/handleCartFormAction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/routing/storefrontRedirect.d.ts +37 -0
- package/dist/routing/storefrontRedirect.d.ts.map +1 -0
- package/dist/routing/storefrontRedirect.js +64 -0
- package/dist/routing/storefrontRedirect.js.map +1 -0
- package/dist/seo/getSeoMeta.d.ts +68 -0
- package/dist/seo/getSeoMeta.d.ts.map +1 -0
- package/dist/seo/getSeoMeta.js +89 -0
- package/dist/seo/getSeoMeta.js.map +1 -0
- package/dist/sitemap/sitemap.d.ts +55 -0
- package/dist/sitemap/sitemap.d.ts.map +1 -0
- package/dist/sitemap/sitemap.js +93 -0
- package/dist/sitemap/sitemap.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/flattenConnection.d.ts +25 -0
- package/dist/utils/flattenConnection.d.ts.map +1 -0
- package/dist/utils/flattenConnection.js +25 -0
- package/dist/utils/flattenConnection.js.map +1 -0
- package/dist/utils/parseGid.d.ts +17 -0
- package/dist/utils/parseGid.d.ts.map +1 -0
- package/dist/utils/parseGid.js +19 -0
- package/dist/utils/parseGid.js.map +1 -0
- package/package.json +1 -1
- package/src/cache/withCache.ts +144 -0
- package/src/components/AddToCartButton.tsx +94 -56
- package/src/components/BuyNowButton.tsx +135 -0
- package/src/components/CartCheckoutButton.tsx +97 -0
- package/src/components/CartCost.tsx +65 -0
- package/src/components/CartForm.tsx +311 -0
- package/src/components/CartLineProvider.tsx +77 -0
- package/src/components/CartLineQuantity.tsx +37 -0
- package/src/components/DiscountSelector.tsx +34 -45
- package/src/components/Image.tsx +27 -0
- package/src/components/Pagination.tsx +139 -0
- package/src/components/RichText.tsx +122 -0
- package/src/components/Seo.tsx +61 -0
- package/src/components/hooks/useMoney.ts +87 -0
- package/src/components/hooks/useOptimisticCart.ts +183 -0
- package/src/components/index.ts +44 -0
- package/src/createCartHandler.ts +71 -0
- package/src/csp/csp.tsx +119 -0
- package/src/customer/createCustomerAccountClient.ts +89 -0
- package/src/handleCartFormAction.ts +129 -0
- package/src/index.ts +24 -0
- package/src/routing/storefrontRedirect.ts +86 -0
- package/src/seo/getSeoMeta.ts +125 -0
- package/src/sitemap/sitemap.ts +121 -0
- package/src/types.ts +12 -1
- package/src/utils/flattenConnection.ts +33 -0
- package/src/utils/parseGid.ts +25 -0
|
@@ -1,101 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <AddToCartButton> — 加入购物车按钮
|
|
3
3
|
*
|
|
4
|
-
* 对齐
|
|
5
|
-
* - 内置 loading 态、disabled、error 处理
|
|
6
|
-
* - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
|
|
7
|
-
* - 触发 analytics 事件钩子
|
|
8
|
-
* - 不带样式,商家自己控制
|
|
4
|
+
* 对齐 Shopify Hydrogen:本质是包了一个 <CartForm action={LinesAdd}> + <button type="submit">。
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* variantId={selectedVariant.id}
|
|
14
|
-
* quantity={1}
|
|
15
|
-
* disabled={!selectedVariant.availableForSale}
|
|
16
|
-
* onAdd={(vid, qty) => addLine(vid, qty)}
|
|
17
|
-
* >
|
|
18
|
-
* 加入购物车
|
|
19
|
-
* </AddToCartButton>
|
|
6
|
+
* - SSR HTML 是原生 <form>,无 JS 也能加购(form 提交 → 服务端 redirect 回 referrer)
|
|
7
|
+
* - hydrate 后 JS 拦截 submit → fetch /cart → CartProvider.applyCart → 无刷新
|
|
8
|
+
* - loading 状态由 CartForm.fetcher 暴露,通过 useFetcher() 读取
|
|
20
9
|
*
|
|
21
|
-
*
|
|
10
|
+
* 商家也可以直接组合 <CartForm>,不用我们这个 wrapper:
|
|
11
|
+
*
|
|
12
|
+
* <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines: [...] }}>
|
|
13
|
+
* <button type="submit">加入购物车</button>
|
|
14
|
+
* </CartForm>
|
|
22
15
|
*/
|
|
23
16
|
|
|
24
17
|
import * as React from 'react';
|
|
25
|
-
import {
|
|
18
|
+
import { CartForm, useFetcher, type CartLineInput } from './CartForm';
|
|
26
19
|
import { useAnalytics } from './AnalyticsProvider';
|
|
27
20
|
|
|
28
|
-
export interface AddToCartButtonProps
|
|
21
|
+
export interface AddToCartButtonProps {
|
|
29
22
|
/** 必传:variant 的 GID(gid://shopbb/ProductVariant/...) */
|
|
30
23
|
variantId: string;
|
|
31
24
|
/** 数量,默认 1 */
|
|
32
25
|
quantity?: number;
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/**
|
|
26
|
+
/** 行级 attributes(如 gift_message) */
|
|
27
|
+
attributes?: Array<{ key: string; value: string }>;
|
|
28
|
+
/** 加购成功回调(仅 hydrated 后触发) */
|
|
36
29
|
onAdded?: () => void;
|
|
37
30
|
/** 加购失败回调 */
|
|
38
31
|
onError?: (err: Error) => void;
|
|
39
|
-
/**
|
|
32
|
+
/** 加购中显示的文本 */
|
|
40
33
|
loadingText?: React.ReactNode;
|
|
41
|
-
/**
|
|
34
|
+
/** 不可用时显示的文本 */
|
|
42
35
|
unavailableText?: React.ReactNode;
|
|
36
|
+
/** disabled */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** 包装 form className */
|
|
39
|
+
className?: string;
|
|
40
|
+
/** 按钮 className */
|
|
41
|
+
buttonClassName?: string;
|
|
42
|
+
/** 提交的 route,默认 /cart */
|
|
43
|
+
route?: string;
|
|
44
|
+
/** 按钮内容 */
|
|
45
|
+
children?: React.ReactNode;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export function AddToCartButton(props: AddToCartButtonProps) {
|
|
46
49
|
const {
|
|
47
50
|
variantId,
|
|
48
51
|
quantity = 1,
|
|
49
|
-
|
|
52
|
+
attributes,
|
|
50
53
|
onAdded,
|
|
51
54
|
onError,
|
|
52
55
|
loadingText = '加入中...',
|
|
53
56
|
unavailableText = '缺货',
|
|
57
|
+
disabled = false,
|
|
58
|
+
className,
|
|
59
|
+
buttonClassName,
|
|
60
|
+
route = '/cart',
|
|
54
61
|
children = '加入购物车',
|
|
55
|
-
disabled: disabledProp,
|
|
56
|
-
...rest
|
|
57
62
|
} = props;
|
|
58
63
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const line: CartLineInput = { merchandiseId: variantId, quantity };
|
|
65
|
+
if (attributes && attributes.length > 0) line.attributes = attributes;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<CartForm
|
|
69
|
+
route={route}
|
|
70
|
+
action={CartForm.ACTIONS.LinesAdd}
|
|
71
|
+
inputs={{ lines: [line] }}
|
|
72
|
+
className={className}
|
|
73
|
+
>
|
|
74
|
+
<AddToCartButtonInner
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
buttonClassName={buttonClassName}
|
|
77
|
+
loadingText={loadingText}
|
|
78
|
+
unavailableText={unavailableText}
|
|
79
|
+
variantId={variantId}
|
|
80
|
+
quantity={quantity}
|
|
81
|
+
onAdded={onAdded}
|
|
82
|
+
onError={onError}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</AddToCartButtonInner>
|
|
86
|
+
</CartForm>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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();
|
|
66
109
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} else {
|
|
76
|
-
|
|
77
|
-
|
|
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?.();
|
|
78
121
|
}
|
|
79
|
-
analytics.emit('add_to_cart', { variantId, quantity });
|
|
80
|
-
onAdded?.();
|
|
81
|
-
} catch (err: any) {
|
|
82
|
-
console.error('[AddToCartButton] add failed:', err);
|
|
83
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
84
|
-
} finally {
|
|
85
|
-
setAdding(false);
|
|
86
122
|
}
|
|
87
|
-
|
|
123
|
+
prevState.current = fetcher.state;
|
|
124
|
+
}, [fetcher.state, fetcher.error, fetcher.data, onAdded, onError, analytics, variantId, quantity]);
|
|
125
|
+
|
|
126
|
+
const adding = fetcher.state !== 'idle';
|
|
88
127
|
|
|
89
128
|
return (
|
|
90
129
|
<button
|
|
91
|
-
type="
|
|
92
|
-
{
|
|
130
|
+
type="submit"
|
|
131
|
+
className={buttonClassName}
|
|
132
|
+
disabled={disabled || adding}
|
|
93
133
|
data-add-to-cart
|
|
94
134
|
data-loading={adding ? '' : undefined}
|
|
95
|
-
disabled={disabledProp || adding}
|
|
96
|
-
onClick={handleClick}
|
|
97
135
|
>
|
|
98
|
-
{adding ? loadingText :
|
|
136
|
+
{adding ? loadingText : disabled ? unavailableText : children}
|
|
99
137
|
</button>
|
|
100
138
|
);
|
|
101
139
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <BuyNowButton> — 对齐 Hydrogen React
|
|
3
|
+
*
|
|
4
|
+
* "立即购买" — 创建/添加 line + 跳 checkout,一步到位。
|
|
5
|
+
*
|
|
6
|
+
* 实现:
|
|
7
|
+
* 1. 内部包 <CartForm action="LinesAdd" inputs={{lines, navigateOnSuccess: '/checkout'}}>
|
|
8
|
+
* 2. submit 成功后 client 自动 navigate 到 checkoutUrl(或 fallback /checkout)
|
|
9
|
+
* 3. 无 JS 时:浏览器走 form POST,服务端 303 跳 /checkout
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* <BuyNowButton variantId="gid://..." quantity={1}>立即购买</BuyNowButton>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { CartForm, useFetcher, type CartLineInput } from './CartForm';
|
|
17
|
+
import { useAnalytics } from './AnalyticsProvider';
|
|
18
|
+
|
|
19
|
+
export interface BuyNowButtonProps {
|
|
20
|
+
/** 必传 variant GID */
|
|
21
|
+
variantId: string;
|
|
22
|
+
/** 数量,默认 1 */
|
|
23
|
+
quantity?: number;
|
|
24
|
+
/** 行级 attributes */
|
|
25
|
+
attributes?: Array<{ key: string; value: string }>;
|
|
26
|
+
/** 加购后跳转的 URL(client 端),默认走 cart.checkoutUrl,回退 /checkout */
|
|
27
|
+
redirectTo?: string;
|
|
28
|
+
/** 失败回调 */
|
|
29
|
+
onError?: (err: Error) => void;
|
|
30
|
+
/** Loading 文案 */
|
|
31
|
+
loadingText?: React.ReactNode;
|
|
32
|
+
/** Disabled 文案 */
|
|
33
|
+
unavailableText?: React.ReactNode;
|
|
34
|
+
/** disabled */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Form className */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** 按钮 className */
|
|
39
|
+
buttonClassName?: string;
|
|
40
|
+
/** 按钮 content */
|
|
41
|
+
children?: React.ReactNode;
|
|
42
|
+
/** 提交 route,默认 /cart */
|
|
43
|
+
route?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function BuyNowButton(props: BuyNowButtonProps) {
|
|
47
|
+
const {
|
|
48
|
+
variantId,
|
|
49
|
+
quantity = 1,
|
|
50
|
+
attributes,
|
|
51
|
+
redirectTo,
|
|
52
|
+
onError,
|
|
53
|
+
loadingText = '处理中...',
|
|
54
|
+
unavailableText = '暂不可购',
|
|
55
|
+
disabled = false,
|
|
56
|
+
className,
|
|
57
|
+
buttonClassName,
|
|
58
|
+
children = '立即购买',
|
|
59
|
+
route = '/cart',
|
|
60
|
+
} = props;
|
|
61
|
+
|
|
62
|
+
const line: CartLineInput = { merchandiseId: variantId, quantity };
|
|
63
|
+
if (attributes && attributes.length > 0) line.attributes = attributes;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<CartForm
|
|
67
|
+
route={route}
|
|
68
|
+
action={CartForm.ACTIONS.LinesAdd}
|
|
69
|
+
inputs={{ lines: [line] }}
|
|
70
|
+
className={className}
|
|
71
|
+
>
|
|
72
|
+
<BuyNowButtonInner
|
|
73
|
+
disabled={disabled}
|
|
74
|
+
buttonClassName={buttonClassName}
|
|
75
|
+
loadingText={loadingText}
|
|
76
|
+
unavailableText={unavailableText}
|
|
77
|
+
variantId={variantId}
|
|
78
|
+
quantity={quantity}
|
|
79
|
+
redirectTo={redirectTo}
|
|
80
|
+
onError={onError}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</BuyNowButtonInner>
|
|
84
|
+
</CartForm>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function BuyNowButtonInner({
|
|
89
|
+
disabled, buttonClassName, loadingText, unavailableText, children,
|
|
90
|
+
variantId, quantity, redirectTo, onError,
|
|
91
|
+
}: {
|
|
92
|
+
disabled: boolean;
|
|
93
|
+
buttonClassName?: string;
|
|
94
|
+
loadingText: React.ReactNode;
|
|
95
|
+
unavailableText: React.ReactNode;
|
|
96
|
+
children: React.ReactNode;
|
|
97
|
+
variantId: string;
|
|
98
|
+
quantity: number;
|
|
99
|
+
redirectTo?: string;
|
|
100
|
+
onError?: (err: Error) => void;
|
|
101
|
+
}) {
|
|
102
|
+
const fetcher = useFetcher();
|
|
103
|
+
const analytics = useAnalytics();
|
|
104
|
+
const prevState = React.useRef(fetcher.state);
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
const wasNonIdle = prevState.current !== 'idle';
|
|
108
|
+
const isIdle = fetcher.state === 'idle';
|
|
109
|
+
if (wasNonIdle && isIdle) {
|
|
110
|
+
if (fetcher.error) {
|
|
111
|
+
onError?.(new Error(fetcher.error));
|
|
112
|
+
} else if (fetcher.data?.cart) {
|
|
113
|
+
analytics.emit('buy_now', { variantId, quantity });
|
|
114
|
+
const target = redirectTo || (fetcher.data.cart as any).checkoutUrl || '/checkout';
|
|
115
|
+
if (typeof window !== 'undefined') {
|
|
116
|
+
window.location.href = target;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
prevState.current = fetcher.state;
|
|
121
|
+
}, [fetcher.state, fetcher.error, fetcher.data, redirectTo, onError, analytics, variantId, quantity]);
|
|
122
|
+
|
|
123
|
+
const pending = fetcher.state !== 'idle';
|
|
124
|
+
return (
|
|
125
|
+
<button
|
|
126
|
+
type="submit"
|
|
127
|
+
className={buttonClassName}
|
|
128
|
+
disabled={disabled || pending}
|
|
129
|
+
data-buy-now
|
|
130
|
+
data-loading={pending ? '' : undefined}
|
|
131
|
+
>
|
|
132
|
+
{pending ? loadingText : disabled ? unavailableText : children}
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <CartCheckoutButton> — 对齐 Hydrogen React
|
|
3
|
+
*
|
|
4
|
+
* 渲染一个跳到 cart.checkoutUrl 的按钮。最常见的"去结算"入口。
|
|
5
|
+
*
|
|
6
|
+
* 行为:
|
|
7
|
+
* - 取 cart.checkoutUrl,无 cart / 空 cart 时按钮 disabled
|
|
8
|
+
* - 默认 <a> 元素(无 JS 也能用),可改成 <button> + onClick
|
|
9
|
+
* - 渲染纯样式,行为非业务逻辑
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* <CartCheckoutButton>去结算</CartCheckoutButton>
|
|
13
|
+
*
|
|
14
|
+
* // 自定义渲染
|
|
15
|
+
* <CartCheckoutButton>
|
|
16
|
+
* {(href, disabled) => (
|
|
17
|
+
* <a href={href} className="my-cta" aria-disabled={disabled}>
|
|
18
|
+
* 结算 {cart.totalQuantity} 件
|
|
19
|
+
* </a>
|
|
20
|
+
* )}
|
|
21
|
+
* </CartCheckoutButton>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as React from 'react';
|
|
25
|
+
import { useCart } from './CartProvider';
|
|
26
|
+
|
|
27
|
+
export interface CartCheckoutButtonProps {
|
|
28
|
+
/** className */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** 强制 disabled(即使 cart 有 checkoutUrl) */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** 按钮内容;不传默认 "去结算" */
|
|
33
|
+
children?: React.ReactNode | ((href: string, disabled: boolean) => React.ReactNode);
|
|
34
|
+
/** 自定义渲染元素,默认 a */
|
|
35
|
+
as?: 'a' | 'button';
|
|
36
|
+
/** 点击回调(事件触发) */
|
|
37
|
+
onClick?: React.MouseEventHandler;
|
|
38
|
+
/** 透传其它 props 到根元素 */
|
|
39
|
+
[key: string]: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function CartCheckoutButton(props: CartCheckoutButtonProps) {
|
|
43
|
+
const { className, disabled: disabledProp, children, as = 'a', onClick, ...rest } = props;
|
|
44
|
+
const { cart } = useCart();
|
|
45
|
+
|
|
46
|
+
const href = (cart as any)?.checkoutUrl as string | undefined;
|
|
47
|
+
const noCart = !cart || cart.totalQuantity === 0 || !href;
|
|
48
|
+
const disabled = !!disabledProp || noCart;
|
|
49
|
+
|
|
50
|
+
if (typeof children === 'function') {
|
|
51
|
+
return <>{children(href || '#', disabled)}</>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const label = children ?? '去结算';
|
|
55
|
+
|
|
56
|
+
if (as === 'button') {
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className={className}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
data-cart-checkout-button
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
if (disabled) return;
|
|
65
|
+
onClick?.(e);
|
|
66
|
+
if (!e.defaultPrevented && href) {
|
|
67
|
+
window.location.href = href;
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
{...rest}
|
|
71
|
+
>
|
|
72
|
+
{label}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// <a> 形式:无 JS 也能跳
|
|
78
|
+
return (
|
|
79
|
+
<a
|
|
80
|
+
href={disabled ? undefined : href}
|
|
81
|
+
className={className}
|
|
82
|
+
aria-disabled={disabled || undefined}
|
|
83
|
+
data-cart-checkout-button
|
|
84
|
+
data-disabled={disabled ? '' : undefined}
|
|
85
|
+
onClick={(e) => {
|
|
86
|
+
if (disabled) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
onClick?.(e);
|
|
91
|
+
}}
|
|
92
|
+
{...rest}
|
|
93
|
+
>
|
|
94
|
+
{label}
|
|
95
|
+
</a>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <CartCost> — 对齐 Hydrogen React
|
|
3
|
+
*
|
|
4
|
+
* 从 <CartProvider> 拿 cart.cost 渲染金额。amountType 控制渲染哪个:
|
|
5
|
+
* "total" → cost.totalAmount (默认)
|
|
6
|
+
* "subtotal" → cost.subtotalAmount
|
|
7
|
+
* "tax" → cost.totalTaxAmount
|
|
8
|
+
* "duty" → cost.totalDutyAmount
|
|
9
|
+
* "discount" → cost.totalDiscountAmount(shopbb-extension)
|
|
10
|
+
*
|
|
11
|
+
* 内部用 <Money> 渲染,自动按 currencyCode 加货币符号。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* <CartCost amountType="subtotal" /> ¥300.00
|
|
15
|
+
* <CartCost amountType="discount" as="strong" /> ¥30.00
|
|
16
|
+
* <CartCost amountType="total" withoutTrailingZeros />
|
|
17
|
+
*
|
|
18
|
+
* 自定义渲染:
|
|
19
|
+
* <CartCost amountType="total">
|
|
20
|
+
* {(money) => <strong>{money.amount} {money.currencyCode}</strong>}
|
|
21
|
+
* </CartCost>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as React from 'react';
|
|
25
|
+
import { useCart } from './CartProvider';
|
|
26
|
+
import { Money, type MoneyProps } from './Money';
|
|
27
|
+
|
|
28
|
+
export interface MoneyValue {
|
|
29
|
+
amount: string;
|
|
30
|
+
currencyCode: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CartCostProps extends Omit<MoneyProps, 'data' | 'children'> {
|
|
34
|
+
/**
|
|
35
|
+
* 渲染哪一项 cost。默认 "total"。
|
|
36
|
+
* - "total" : 含运费 / 税 / 折扣的最终金额
|
|
37
|
+
* - "subtotal" : 商品小计(折扣前 / 运费前)
|
|
38
|
+
* - "tax" : 税
|
|
39
|
+
* - "duty" : 关税
|
|
40
|
+
* - "discount" : 优惠金额(shopbb extension)
|
|
41
|
+
*/
|
|
42
|
+
amountType?: 'total' | 'subtotal' | 'tax' | 'duty' | 'discount';
|
|
43
|
+
/** 自定义渲染:(money) => ReactNode */
|
|
44
|
+
children?: (money: MoneyValue) => React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function CartCost(props: CartCostProps) {
|
|
48
|
+
const { amountType = 'total', children, ...moneyProps } = props;
|
|
49
|
+
const { cart } = useCart();
|
|
50
|
+
if (!cart?.cost) return null;
|
|
51
|
+
|
|
52
|
+
const cost = cart.cost as any;
|
|
53
|
+
let money: MoneyValue | null = null;
|
|
54
|
+
switch (amountType) {
|
|
55
|
+
case 'total': money = cost.totalAmount ?? null; break;
|
|
56
|
+
case 'subtotal': money = cost.subtotalAmount ?? null; break;
|
|
57
|
+
case 'tax': money = cost.totalTaxAmount ?? cost.taxAmount ?? null; break;
|
|
58
|
+
case 'duty': money = cost.totalDutyAmount ?? cost.dutyAmount ?? null; break;
|
|
59
|
+
case 'discount': money = cost.totalDiscountAmount ?? null; break;
|
|
60
|
+
}
|
|
61
|
+
if (!money) return null;
|
|
62
|
+
|
|
63
|
+
if (children) return <>{children(money)}</>;
|
|
64
|
+
return <Money data={money} {...moneyProps} />;
|
|
65
|
+
}
|