@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createWithCache + InMemoryCache — 对齐 Hydrogen
|
|
3
|
+
*
|
|
4
|
+
* Hydrogen 的 `createWithCache({cache, waitUntil})` 返回一个高阶函数,
|
|
5
|
+
* 用 `withCache.run(strategy, cacheKey, fn)` 包裹任意异步函数,自动加 cache。
|
|
6
|
+
*
|
|
7
|
+
* 在 Cloudflare Workers 上 cache 可以是:
|
|
8
|
+
* - `caches.default` / `caches.open(...)` (Cache API — 真正的 edge cache)
|
|
9
|
+
* - `InMemoryCache` (本 worker 实例内存,per-instance;轻量缓存)
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
*
|
|
13
|
+
* const withCache = createWithCache({ cache: await caches.open('helium'), waitUntil });
|
|
14
|
+
*
|
|
15
|
+
* const products = await withCache.run(
|
|
16
|
+
* CacheLong(),
|
|
17
|
+
* ['products', 'page', '1'],
|
|
18
|
+
* async () => storefront.query(PRODUCTS_QUERY, { variables }),
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* 简化点(vs Hydrogen):
|
|
22
|
+
* - 不实现 stale-while-revalidate 后台 revalidate(Workers 内复杂,先做基础)
|
|
23
|
+
* - 不实现 stale-if-error
|
|
24
|
+
* - 提供 InMemoryCache 兜底(test / 没 caches.open 的环境)
|
|
25
|
+
*/
|
|
26
|
+
import type { CacheStrategy } from '../types';
|
|
27
|
+
export type CachingStrategy = CacheStrategy;
|
|
28
|
+
export type CacheKey = string | (string | number)[];
|
|
29
|
+
export interface WithCacheOptions {
|
|
30
|
+
cache: Cache | InMemoryCache;
|
|
31
|
+
waitUntil?: (p: Promise<any>) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface WithCache {
|
|
34
|
+
/** 用 cache 包裹异步函数。命中 cache 直接返回;未命中执行 fn 写入 */
|
|
35
|
+
run<T>(strategy: CachingStrategy, key: CacheKey, fn: () => Promise<T>): Promise<T>;
|
|
36
|
+
/** 直接读 cache(不执行 fn) */
|
|
37
|
+
get<T>(key: CacheKey): Promise<T | null>;
|
|
38
|
+
/** 写 cache */
|
|
39
|
+
set<T>(strategy: CachingStrategy, key: CacheKey, value: T): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createWithCache(opts: WithCacheOptions): WithCache;
|
|
42
|
+
export declare class InMemoryCache {
|
|
43
|
+
private store;
|
|
44
|
+
get<T = any>(key: string): T | null;
|
|
45
|
+
set<T = any>(key: string, value: T, maxAgeSeconds: number): void;
|
|
46
|
+
delete(key: string): boolean;
|
|
47
|
+
clear(): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=withCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withCache.d.ts","sourceRoot":"","sources":["../../src/cache/withCache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;AAEpD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,KAAK,GAAG,aAAa,CAAC;IAC7B,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnF,wBAAwB;IACxB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzC,cAAc;IACd,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAgBD,wBAAgB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,CA8CjE;AAWD,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAA+B;IAE5C,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAUnC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAKhE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createWithCache + InMemoryCache — 对齐 Hydrogen
|
|
3
|
+
*
|
|
4
|
+
* Hydrogen 的 `createWithCache({cache, waitUntil})` 返回一个高阶函数,
|
|
5
|
+
* 用 `withCache.run(strategy, cacheKey, fn)` 包裹任意异步函数,自动加 cache。
|
|
6
|
+
*
|
|
7
|
+
* 在 Cloudflare Workers 上 cache 可以是:
|
|
8
|
+
* - `caches.default` / `caches.open(...)` (Cache API — 真正的 edge cache)
|
|
9
|
+
* - `InMemoryCache` (本 worker 实例内存,per-instance;轻量缓存)
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
*
|
|
13
|
+
* const withCache = createWithCache({ cache: await caches.open('helium'), waitUntil });
|
|
14
|
+
*
|
|
15
|
+
* const products = await withCache.run(
|
|
16
|
+
* CacheLong(),
|
|
17
|
+
* ['products', 'page', '1'],
|
|
18
|
+
* async () => storefront.query(PRODUCTS_QUERY, { variables }),
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* 简化点(vs Hydrogen):
|
|
22
|
+
* - 不实现 stale-while-revalidate 后台 revalidate(Workers 内复杂,先做基础)
|
|
23
|
+
* - 不实现 stale-if-error
|
|
24
|
+
* - 提供 InMemoryCache 兜底(test / 没 caches.open 的环境)
|
|
25
|
+
*/
|
|
26
|
+
function toCacheKey(key) {
|
|
27
|
+
if (typeof key === 'string')
|
|
28
|
+
return key;
|
|
29
|
+
return key.map(String).join(':');
|
|
30
|
+
}
|
|
31
|
+
function buildCacheControl(strategy) {
|
|
32
|
+
const dirs = [];
|
|
33
|
+
if (strategy.mode === 'NONE')
|
|
34
|
+
return 'no-store';
|
|
35
|
+
if (strategy.mode === 'PUBLIC')
|
|
36
|
+
dirs.push('public');
|
|
37
|
+
if (strategy.maxAge != null)
|
|
38
|
+
dirs.push(`max-age=${strategy.maxAge}`);
|
|
39
|
+
if (strategy.staleWhileRevalidate != null)
|
|
40
|
+
dirs.push(`stale-while-revalidate=${strategy.staleWhileRevalidate}`);
|
|
41
|
+
return dirs.join(', ');
|
|
42
|
+
}
|
|
43
|
+
export function createWithCache(opts) {
|
|
44
|
+
const { cache, waitUntil } = opts;
|
|
45
|
+
async function get(key) {
|
|
46
|
+
const cacheKey = toCacheKey(key);
|
|
47
|
+
if (cache instanceof InMemoryCache) {
|
|
48
|
+
return cache.get(cacheKey);
|
|
49
|
+
}
|
|
50
|
+
const req = new Request(`https://helium.cache/${encodeURIComponent(cacheKey)}`);
|
|
51
|
+
const cached = await cache.match(req);
|
|
52
|
+
if (!cached)
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
return (await cached.json());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function set(strategy, key, value) {
|
|
62
|
+
if (strategy.mode === 'NONE')
|
|
63
|
+
return;
|
|
64
|
+
const cacheKey = toCacheKey(key);
|
|
65
|
+
if (cache instanceof InMemoryCache) {
|
|
66
|
+
cache.set(cacheKey, value, strategy.maxAge ?? 0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const req = new Request(`https://helium.cache/${encodeURIComponent(cacheKey)}`);
|
|
70
|
+
const res = new Response(JSON.stringify(value), {
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Cache-Control': buildCacheControl(strategy),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const p = cache.put(req, res);
|
|
77
|
+
if (waitUntil)
|
|
78
|
+
waitUntil(p);
|
|
79
|
+
else
|
|
80
|
+
await p;
|
|
81
|
+
}
|
|
82
|
+
async function run(strategy, key, fn) {
|
|
83
|
+
if (strategy.mode === 'NONE')
|
|
84
|
+
return fn();
|
|
85
|
+
const cached = await get(key);
|
|
86
|
+
if (cached !== null)
|
|
87
|
+
return cached;
|
|
88
|
+
const value = await fn();
|
|
89
|
+
await set(strategy, key, value);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
return { run, get, set };
|
|
93
|
+
}
|
|
94
|
+
export class InMemoryCache {
|
|
95
|
+
store = new Map();
|
|
96
|
+
get(key) {
|
|
97
|
+
const e = this.store.get(key);
|
|
98
|
+
if (!e)
|
|
99
|
+
return null;
|
|
100
|
+
if (e.expiresAt > 0 && Date.now() > e.expiresAt) {
|
|
101
|
+
this.store.delete(key);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return e.value;
|
|
105
|
+
}
|
|
106
|
+
set(key, value, maxAgeSeconds) {
|
|
107
|
+
const expiresAt = maxAgeSeconds > 0 ? Date.now() + maxAgeSeconds * 1000 : 0;
|
|
108
|
+
this.store.set(key, { value, expiresAt });
|
|
109
|
+
}
|
|
110
|
+
delete(key) {
|
|
111
|
+
return this.store.delete(key);
|
|
112
|
+
}
|
|
113
|
+
clear() {
|
|
114
|
+
this.store.clear();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=withCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withCache.js","sourceRoot":"","sources":["../../src/cache/withCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,41 +1,47 @@
|
|
|
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
|
import * as React from 'react';
|
|
24
|
-
export interface AddToCartButtonProps
|
|
17
|
+
export interface AddToCartButtonProps {
|
|
25
18
|
/** 必传:variant 的 GID(gid://shopbb/ProductVariant/...) */
|
|
26
19
|
variantId: string;
|
|
27
20
|
/** 数量,默认 1 */
|
|
28
21
|
quantity?: number;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
/** 行级 attributes(如 gift_message) */
|
|
23
|
+
attributes?: Array<{
|
|
24
|
+
key: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** 加购成功回调(仅 hydrated 后触发) */
|
|
32
28
|
onAdded?: () => void;
|
|
33
29
|
/** 加购失败回调 */
|
|
34
30
|
onError?: (err: Error) => void;
|
|
35
|
-
/**
|
|
31
|
+
/** 加购中显示的文本 */
|
|
36
32
|
loadingText?: React.ReactNode;
|
|
37
|
-
/**
|
|
33
|
+
/** 不可用时显示的文本 */
|
|
38
34
|
unavailableText?: React.ReactNode;
|
|
35
|
+
/** disabled */
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
/** 包装 form className */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** 按钮 className */
|
|
40
|
+
buttonClassName?: string;
|
|
41
|
+
/** 提交的 route,默认 /cart */
|
|
42
|
+
route?: string;
|
|
43
|
+
/** 按钮内容 */
|
|
44
|
+
children?: React.ReactNode;
|
|
39
45
|
}
|
|
40
46
|
export declare function AddToCartButton(props: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
41
47
|
//# sourceMappingURL=AddToCartButton.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa;IACb,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,eAAe;IACf,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,gBAAgB;IAChB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,eAAe;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW;IACX,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAwC1D"}
|
|
@@ -2,62 +2,51 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* <AddToCartButton> — 加入购物车按钮
|
|
4
4
|
*
|
|
5
|
-
* 对齐
|
|
6
|
-
* - 内置 loading 态、disabled、error 处理
|
|
7
|
-
* - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
|
|
8
|
-
* - 触发 analytics 事件钩子
|
|
9
|
-
* - 不带样式,商家自己控制
|
|
5
|
+
* 对齐 Shopify Hydrogen:本质是包了一个 <CartForm action={LinesAdd}> + <button type="submit">。
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* variantId={selectedVariant.id}
|
|
15
|
-
* quantity={1}
|
|
16
|
-
* disabled={!selectedVariant.availableForSale}
|
|
17
|
-
* onAdd={(vid, qty) => addLine(vid, qty)}
|
|
18
|
-
* >
|
|
19
|
-
* 加入购物车
|
|
20
|
-
* </AddToCartButton>
|
|
7
|
+
* - SSR HTML 是原生 <form>,无 JS 也能加购(form 提交 → 服务端 redirect 回 referrer)
|
|
8
|
+
* - hydrate 后 JS 拦截 submit → fetch /cart → CartProvider.applyCart → 无刷新
|
|
9
|
+
* - loading 状态由 CartForm.fetcher 暴露,通过 useFetcher() 读取
|
|
21
10
|
*
|
|
22
|
-
*
|
|
11
|
+
* 商家也可以直接组合 <CartForm>,不用我们这个 wrapper:
|
|
12
|
+
*
|
|
13
|
+
* <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines: [...] }}>
|
|
14
|
+
* <button type="submit">加入购物车</button>
|
|
15
|
+
* </CartForm>
|
|
23
16
|
*/
|
|
24
17
|
import * as React from 'react';
|
|
25
|
-
import {
|
|
18
|
+
import { CartForm, useFetcher } from './CartForm';
|
|
26
19
|
import { useAnalytics } from './AnalyticsProvider';
|
|
27
20
|
export function AddToCartButton(props) {
|
|
28
|
-
const { variantId, quantity = 1,
|
|
29
|
-
const
|
|
30
|
-
|
|
21
|
+
const { variantId, quantity = 1, attributes, onAdded, onError, loadingText = '加入中...', unavailableText = '缺货', disabled = false, className, buttonClassName, route = '/cart', children = '加入购物车', } = props;
|
|
22
|
+
const line = { merchandiseId: variantId, quantity };
|
|
23
|
+
if (attributes && attributes.length > 0)
|
|
24
|
+
line.attributes = attributes;
|
|
25
|
+
return (_jsx(CartForm, { route: route, action: CartForm.ACTIONS.LinesAdd, inputs: { lines: [line] }, className: className, children: _jsx(AddToCartButtonInner, { disabled: disabled, buttonClassName: buttonClassName, loadingText: loadingText, unavailableText: unavailableText, variantId: variantId, quantity: quantity, onAdded: onAdded, onError: onError, children: children }) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Inner component — 合法地 useFetcher / useEffect,监听 fetcher.state 变化。
|
|
29
|
+
*/
|
|
30
|
+
function AddToCartButtonInner({ disabled, buttonClassName, loadingText, unavailableText, children, variantId, quantity, onAdded, onError, }) {
|
|
31
|
+
const fetcher = useFetcher();
|
|
31
32
|
const analytics = useAnalytics();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
await onAdd(variantId, quantity);
|
|
33
|
+
// 监听 fetcher 完成
|
|
34
|
+
const prevState = React.useRef(fetcher.state);
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const wasNonIdle = prevState.current !== 'idle';
|
|
37
|
+
const isIdle = fetcher.state === 'idle';
|
|
38
|
+
if (wasNonIdle && isIdle) {
|
|
39
|
+
if (fetcher.error) {
|
|
40
|
+
onError?.(new Error(fetcher.error));
|
|
41
41
|
}
|
|
42
|
-
else if (cart) {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
else if (fetcher.data?.cart) {
|
|
43
|
+
analytics.emit('add_to_cart', { variantId, quantity });
|
|
44
|
+
onAdded?.();
|
|
45
45
|
}
|
|
46
|
-
else {
|
|
47
|
-
console.warn('[AddToCartButton] no onAdd and no <CartProvider> — button does nothing');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
analytics.emit('add_to_cart', { variantId, quantity });
|
|
51
|
-
onAdded?.();
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
console.error('[AddToCartButton] add failed:', err);
|
|
55
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
setAdding(false);
|
|
59
46
|
}
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
prevState.current = fetcher.state;
|
|
48
|
+
}, [fetcher.state, fetcher.error, fetcher.data, onAdded, onError, analytics, variantId, quantity]);
|
|
49
|
+
const adding = fetcher.state !== 'idle';
|
|
50
|
+
return (_jsx("button", { type: "submit", className: buttonClassName, disabled: disabled || adding, "data-add-to-cart": true, "data-loading": adding ? '' : undefined, children: adding ? loadingText : disabled ? unavailableText : children }));
|
|
62
51
|
}
|
|
63
52
|
//# sourceMappingURL=AddToCartButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAsB,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA6BnD,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EACJ,SAAS,EACT,QAAQ,GAAG,CAAC,EACZ,UAAU,EACV,OAAO,EACP,OAAO,EACP,WAAW,GAAG,QAAQ,EACtB,eAAe,GAAG,IAAI,EACtB,QAAQ,GAAG,KAAK,EAChB,SAAS,EACT,eAAe,EACf,KAAK,GAAG,OAAO,EACf,QAAQ,GAAG,OAAO,GACnB,GAAG,KAAK,CAAC;IAEV,MAAM,IAAI,GAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACnE,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAEtE,OAAO,CACL,KAAC,QAAQ,IACP,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,EACjC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,EACzB,SAAS,EAAE,SAAS,YAEpB,KAAC,oBAAoB,IACnB,QAAQ,EAAE,QAAQ,EAClB,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,WAAW,EACxB,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,YAEf,QAAQ,GACY,GACd,CACZ,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,EAC5B,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,EAAE,QAAQ,EACjE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAWtC;IACC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IAEjC,gBAAgB;IAChB,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,UAAU,GAAG,SAAS,CAAC,OAAO,KAAK,MAAM,CAAC;QAChD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC;QACxC,IAAI,UAAU,IAAI,MAAM,EAAE,CAAC;YACzB,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBAClB,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YACtC,CAAC;iBAAM,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;gBAC9B,SAAS,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;gBACvD,OAAO,EAAE,EAAE,CAAC;YACd,CAAC;QACH,CAAC;QACD,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC;IACpC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEnG,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC;IAExC,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,QAAQ,IAAI,MAAM,4CAEd,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,YAEpC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,GACtD,CACV,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
import * as React from 'react';
|
|
15
|
+
export interface BuyNowButtonProps {
|
|
16
|
+
/** 必传 variant GID */
|
|
17
|
+
variantId: string;
|
|
18
|
+
/** 数量,默认 1 */
|
|
19
|
+
quantity?: number;
|
|
20
|
+
/** 行级 attributes */
|
|
21
|
+
attributes?: Array<{
|
|
22
|
+
key: string;
|
|
23
|
+
value: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** 加购后跳转的 URL(client 端),默认走 cart.checkoutUrl,回退 /checkout */
|
|
26
|
+
redirectTo?: string;
|
|
27
|
+
/** 失败回调 */
|
|
28
|
+
onError?: (err: Error) => void;
|
|
29
|
+
/** Loading 文案 */
|
|
30
|
+
loadingText?: React.ReactNode;
|
|
31
|
+
/** Disabled 文案 */
|
|
32
|
+
unavailableText?: React.ReactNode;
|
|
33
|
+
/** disabled */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Form className */
|
|
36
|
+
className?: string;
|
|
37
|
+
/** 按钮 className */
|
|
38
|
+
buttonClassName?: string;
|
|
39
|
+
/** 按钮 content */
|
|
40
|
+
children?: React.ReactNode;
|
|
41
|
+
/** 提交 route,默认 /cart */
|
|
42
|
+
route?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function BuyNowButton(props: BuyNowButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
//# sourceMappingURL=BuyNowButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BuyNowButton.d.ts","sourceRoot":"","sources":["../../src/components/BuyNowButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,iBAAiB;IAChC,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,iBAAiB;IACjB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,kBAAkB;IAClB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,eAAe;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,2CAwCpD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* <BuyNowButton> — 对齐 Hydrogen React
|
|
4
|
+
*
|
|
5
|
+
* "立即购买" — 创建/添加 line + 跳 checkout,一步到位。
|
|
6
|
+
*
|
|
7
|
+
* 实现:
|
|
8
|
+
* 1. 内部包 <CartForm action="LinesAdd" inputs={{lines, navigateOnSuccess: '/checkout'}}>
|
|
9
|
+
* 2. submit 成功后 client 自动 navigate 到 checkoutUrl(或 fallback /checkout)
|
|
10
|
+
* 3. 无 JS 时:浏览器走 form POST,服务端 303 跳 /checkout
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* <BuyNowButton variantId="gid://..." quantity={1}>立即购买</BuyNowButton>
|
|
14
|
+
*/
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { CartForm, useFetcher } from './CartForm';
|
|
17
|
+
import { useAnalytics } from './AnalyticsProvider';
|
|
18
|
+
export function BuyNowButton(props) {
|
|
19
|
+
const { variantId, quantity = 1, attributes, redirectTo, onError, loadingText = '处理中...', unavailableText = '暂不可购', disabled = false, className, buttonClassName, children = '立即购买', route = '/cart', } = props;
|
|
20
|
+
const line = { merchandiseId: variantId, quantity };
|
|
21
|
+
if (attributes && attributes.length > 0)
|
|
22
|
+
line.attributes = attributes;
|
|
23
|
+
return (_jsx(CartForm, { route: route, action: CartForm.ACTIONS.LinesAdd, inputs: { lines: [line] }, className: className, children: _jsx(BuyNowButtonInner, { disabled: disabled, buttonClassName: buttonClassName, loadingText: loadingText, unavailableText: unavailableText, variantId: variantId, quantity: quantity, redirectTo: redirectTo, onError: onError, children: children }) }));
|
|
24
|
+
}
|
|
25
|
+
function BuyNowButtonInner({ disabled, buttonClassName, loadingText, unavailableText, children, variantId, quantity, redirectTo, onError, }) {
|
|
26
|
+
const fetcher = useFetcher();
|
|
27
|
+
const analytics = useAnalytics();
|
|
28
|
+
const prevState = React.useRef(fetcher.state);
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
const wasNonIdle = prevState.current !== 'idle';
|
|
31
|
+
const isIdle = fetcher.state === 'idle';
|
|
32
|
+
if (wasNonIdle && isIdle) {
|
|
33
|
+
if (fetcher.error) {
|
|
34
|
+
onError?.(new Error(fetcher.error));
|
|
35
|
+
}
|
|
36
|
+
else if (fetcher.data?.cart) {
|
|
37
|
+
analytics.emit('buy_now', { variantId, quantity });
|
|
38
|
+
const target = redirectTo || fetcher.data.cart.checkoutUrl || '/checkout';
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
window.location.href = target;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
prevState.current = fetcher.state;
|
|
45
|
+
}, [fetcher.state, fetcher.error, fetcher.data, redirectTo, onError, analytics, variantId, quantity]);
|
|
46
|
+
const pending = fetcher.state !== 'idle';
|
|
47
|
+
return (_jsx("button", { type: "submit", className: buttonClassName, disabled: disabled || pending, "data-buy-now": true, "data-loading": pending ? '' : undefined, children: pending ? loadingText : disabled ? unavailableText : children }));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=BuyNowButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BuyNowButton.js","sourceRoot":"","sources":["../../src/components/BuyNowButton.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
import * as React from 'react';
|
|
24
|
+
export interface CartCheckoutButtonProps {
|
|
25
|
+
/** className */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** 强制 disabled(即使 cart 有 checkoutUrl) */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** 按钮内容;不传默认 "去结算" */
|
|
30
|
+
children?: React.ReactNode | ((href: string, disabled: boolean) => React.ReactNode);
|
|
31
|
+
/** 自定义渲染元素,默认 a */
|
|
32
|
+
as?: 'a' | 'button';
|
|
33
|
+
/** 点击回调(事件触发) */
|
|
34
|
+
onClick?: React.MouseEventHandler;
|
|
35
|
+
/** 透传其它 props 到根元素 */
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
export declare function CartCheckoutButton(props: CartCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=CartCheckoutButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartCheckoutButton.d.ts","sourceRoot":"","sources":["../../src/components/CartCheckoutButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,WAAW,uBAAuB;IACtC,gBAAgB;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IACpF,mBAAmB;IACnB,EAAE,CAAC,EAAE,GAAG,GAAG,QAAQ,CAAC;IACpB,iBAAiB;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC;IAClC,sBAAsB;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,2CAuDhE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCart } from './CartProvider';
|
|
3
|
+
export function CartCheckoutButton(props) {
|
|
4
|
+
const { className, disabled: disabledProp, children, as = 'a', onClick, ...rest } = props;
|
|
5
|
+
const { cart } = useCart();
|
|
6
|
+
const href = cart?.checkoutUrl;
|
|
7
|
+
const noCart = !cart || cart.totalQuantity === 0 || !href;
|
|
8
|
+
const disabled = !!disabledProp || noCart;
|
|
9
|
+
if (typeof children === 'function') {
|
|
10
|
+
return _jsx(_Fragment, { children: children(href || '#', disabled) });
|
|
11
|
+
}
|
|
12
|
+
const label = children ?? '去结算';
|
|
13
|
+
if (as === 'button') {
|
|
14
|
+
return (_jsx("button", { type: "button", className: className, disabled: disabled, "data-cart-checkout-button": true, onClick: (e) => {
|
|
15
|
+
if (disabled)
|
|
16
|
+
return;
|
|
17
|
+
onClick?.(e);
|
|
18
|
+
if (!e.defaultPrevented && href) {
|
|
19
|
+
window.location.href = href;
|
|
20
|
+
}
|
|
21
|
+
}, ...rest, children: label }));
|
|
22
|
+
}
|
|
23
|
+
// <a> 形式:无 JS 也能跳
|
|
24
|
+
return (_jsx("a", { href: disabled ? undefined : href, className: className, "aria-disabled": disabled || undefined, "data-cart-checkout-button": true, "data-disabled": disabled ? '' : undefined, onClick: (e) => {
|
|
25
|
+
if (disabled) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onClick?.(e);
|
|
30
|
+
}, ...rest, children: label }));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=CartCheckoutButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartCheckoutButton.js","sourceRoot":"","sources":["../../src/components/CartCheckoutButton.tsx"],"names":[],"mappings":";AAwBA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAiBzC,MAAM,UAAU,kBAAkB,CAAC,KAA8B;IAC/D,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAC1F,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAE3B,MAAM,IAAI,GAAI,IAAY,EAAE,WAAiC,CAAC;IAC9D,MAAM,MAAM,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,aAAa,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;IAC1D,MAAM,QAAQ,GAAG,CAAC,CAAC,YAAY,IAAI,MAAM,CAAC;IAE1C,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,4BAAG,QAAQ,CAAC,IAAI,IAAI,GAAG,EAAE,QAAQ,CAAC,GAAI,CAAC;IAChD,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,IAAI,KAAK,CAAC;IAEhC,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,qCAElB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gBACb,IAAI,QAAQ;oBAAE,OAAO;gBACrB,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;gBACb,IAAI,CAAC,CAAC,CAAC,gBAAgB,IAAI,IAAI,EAAE,CAAC;oBAChC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;gBAC9B,CAAC;YACH,CAAC,KACG,IAAI,YAEP,KAAK,GACC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,YACE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EACjC,SAAS,EAAE,SAAS,mBACL,QAAQ,IAAI,SAAS,sDAErB,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EACxC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACb,IAAI,QAAQ,EAAE,CAAC;gBACb,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACf,CAAC,KACG,IAAI,YAEP,KAAK,GACJ,CACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import * as React from 'react';
|
|
24
|
+
import { type MoneyProps } from './Money';
|
|
25
|
+
export interface MoneyValue {
|
|
26
|
+
amount: string;
|
|
27
|
+
currencyCode: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CartCostProps extends Omit<MoneyProps, 'data' | 'children'> {
|
|
30
|
+
/**
|
|
31
|
+
* 渲染哪一项 cost。默认 "total"。
|
|
32
|
+
* - "total" : 含运费 / 税 / 折扣的最终金额
|
|
33
|
+
* - "subtotal" : 商品小计(折扣前 / 运费前)
|
|
34
|
+
* - "tax" : 税
|
|
35
|
+
* - "duty" : 关税
|
|
36
|
+
* - "discount" : 优惠金额(shopbb extension)
|
|
37
|
+
*/
|
|
38
|
+
amountType?: 'total' | 'subtotal' | 'tax' | 'duty' | 'discount';
|
|
39
|
+
/** 自定义渲染:(money) => ReactNode */
|
|
40
|
+
children?: (money: MoneyValue) => React.ReactNode;
|
|
41
|
+
}
|
|
42
|
+
export declare function CartCost(props: CartCostProps): import("react/jsx-runtime").JSX.Element | null;
|
|
43
|
+
//# sourceMappingURL=CartCost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartCost.d.ts","sourceRoot":"","sources":["../../src/components/CartCost.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1E;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;IAChE,iCAAiC;IACjC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAC;CACnD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,kDAkB5C"}
|