@shopbb/helium 0.2.0 → 0.3.0-alpha.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/components/AddToCartButton.d.ts +41 -0
- package/dist/components/AddToCartButton.d.ts.map +1 -0
- package/dist/components/AddToCartButton.js +51 -0
- package/dist/components/AddToCartButton.js.map +1 -0
- package/dist/components/CartLineQuantityAdjustButton.d.ts +40 -0
- package/dist/components/CartLineQuantityAdjustButton.d.ts.map +1 -0
- package/dist/components/CartLineQuantityAdjustButton.js +58 -0
- package/dist/components/CartLineQuantityAdjustButton.js.map +1 -0
- package/dist/components/Image.d.ts +39 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +28 -0
- package/dist/components/Image.js.map +1 -0
- package/dist/components/Money.d.ts +41 -0
- package/dist/components/Money.d.ts.map +1 -0
- package/dist/components/Money.js +48 -0
- package/dist/components/Money.js.map +1 -0
- package/dist/components/ProductPrice.d.ts +28 -0
- package/dist/components/ProductPrice.d.ts.map +1 -0
- package/dist/components/ProductPrice.js +10 -0
- package/dist/components/ProductPrice.js.map +1 -0
- package/dist/components/VariantSelector.d.ts +80 -0
- package/dist/components/VariantSelector.d.ts.map +1 -0
- package/dist/components/VariantSelector.js +87 -0
- package/dist/components/VariantSelector.js.map +1 -0
- package/dist/components/index.d.ts +24 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +18 -0
- package/dist/components/index.js.map +1 -0
- package/package.json +12 -4
- package/src/components/AddToCartButton.tsx +90 -0
- package/src/components/CartLineQuantityAdjustButton.tsx +119 -0
- package/src/components/Image.tsx +93 -0
- package/src/components/Money.tsx +106 -0
- package/src/components/ProductPrice.tsx +61 -0
- package/src/components/VariantSelector.tsx +148 -0
- package/src/components/index.ts +33 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <AddToCartButton> — 加入购物车按钮
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen <AddToCartButton>:
|
|
5
|
+
* - 内置 loading 态、disabled、error 处理
|
|
6
|
+
* - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
|
|
7
|
+
* - 触发 analytics 事件钩子
|
|
8
|
+
* - 不带样式,商家自己控制
|
|
9
|
+
*
|
|
10
|
+
* 用法(Phase 1,需要传 onAdd):
|
|
11
|
+
* const { addLine } = useCart();
|
|
12
|
+
* <AddToCartButton
|
|
13
|
+
* variantId={selectedVariant.id}
|
|
14
|
+
* quantity={1}
|
|
15
|
+
* disabled={!selectedVariant.availableForSale}
|
|
16
|
+
* onAdd={(vid, qty) => addLine(vid, qty)}
|
|
17
|
+
* >
|
|
18
|
+
* 加入购物车
|
|
19
|
+
* </AddToCartButton>
|
|
20
|
+
*
|
|
21
|
+
* Phase 2 后:组件会自动从 <CartProvider> 拿,无需 onAdd 也能用。
|
|
22
|
+
*/
|
|
23
|
+
import * as React from 'react';
|
|
24
|
+
export interface AddToCartButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'onError'> {
|
|
25
|
+
/** 必传:variant 的 GID(gid://shopbb/ProductVariant/...) */
|
|
26
|
+
variantId: string;
|
|
27
|
+
/** 数量,默认 1 */
|
|
28
|
+
quantity?: number;
|
|
29
|
+
/** 实际加购回调。Phase 1 必传;Phase 2 后可从 Provider 自动获取 */
|
|
30
|
+
onAdd?: (variantId: string, quantity: number) => Promise<void>;
|
|
31
|
+
/** 加购成功回调(如导航到 /cart) */
|
|
32
|
+
onAdded?: () => void;
|
|
33
|
+
/** 加购失败回调 */
|
|
34
|
+
onError?: (err: Error) => void;
|
|
35
|
+
/** 加购中显示的文本,默认 "加入中..." */
|
|
36
|
+
loadingText?: React.ReactNode;
|
|
37
|
+
/** 不可用时显示的文本,默认 "缺货" */
|
|
38
|
+
unavailableText?: React.ReactNode;
|
|
39
|
+
}
|
|
40
|
+
export declare function AddToCartButton(props: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
//# sourceMappingURL=AddToCartButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACtH,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa;IACb,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,2BAA2B;IAC3B,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,wBAAwB;IACxB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACnC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CA+C1D"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* <AddToCartButton> — 加入购物车按钮
|
|
4
|
+
*
|
|
5
|
+
* 对齐 @shopify/hydrogen <AddToCartButton>:
|
|
6
|
+
* - 内置 loading 态、disabled、error 处理
|
|
7
|
+
* - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
|
|
8
|
+
* - 触发 analytics 事件钩子
|
|
9
|
+
* - 不带样式,商家自己控制
|
|
10
|
+
*
|
|
11
|
+
* 用法(Phase 1,需要传 onAdd):
|
|
12
|
+
* const { addLine } = useCart();
|
|
13
|
+
* <AddToCartButton
|
|
14
|
+
* variantId={selectedVariant.id}
|
|
15
|
+
* quantity={1}
|
|
16
|
+
* disabled={!selectedVariant.availableForSale}
|
|
17
|
+
* onAdd={(vid, qty) => addLine(vid, qty)}
|
|
18
|
+
* >
|
|
19
|
+
* 加入购物车
|
|
20
|
+
* </AddToCartButton>
|
|
21
|
+
*
|
|
22
|
+
* Phase 2 后:组件会自动从 <CartProvider> 拿,无需 onAdd 也能用。
|
|
23
|
+
*/
|
|
24
|
+
import * as React from 'react';
|
|
25
|
+
export function AddToCartButton(props) {
|
|
26
|
+
const { variantId, quantity = 1, onAdd, onAdded, onError, loadingText = '加入中...', unavailableText = '缺货', children = '加入购物车', disabled: disabledProp, ...rest } = props;
|
|
27
|
+
const [adding, setAdding] = React.useState(false);
|
|
28
|
+
const handleClick = async (e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
if (adding || disabledProp)
|
|
31
|
+
return;
|
|
32
|
+
if (!onAdd) {
|
|
33
|
+
console.warn('[AddToCartButton] onAdd not provided; this button does nothing in Phase 1');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
setAdding(true);
|
|
37
|
+
try {
|
|
38
|
+
await onAdd(variantId, quantity);
|
|
39
|
+
onAdded?.();
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error('[AddToCartButton] add failed:', err);
|
|
43
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setAdding(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return (_jsx("button", { type: "button", ...rest, "data-add-to-cart": true, "data-loading": adding ? '' : undefined, disabled: disabledProp || adding, onClick: handleClick, children: adding ? loadingText : disabledProp ? unavailableText : children }));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=AddToCartButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAmB/B,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EACJ,SAAS,EACT,QAAQ,GAAG,CAAC,EACZ,KAAK,EACL,OAAO,EACP,OAAO,EACP,WAAW,GAAG,QAAQ,EACtB,eAAe,GAAG,IAAI,EACtB,QAAQ,GAAG,OAAO,EAClB,QAAQ,EAAE,YAAY,EACtB,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,MAAM,WAAW,GAAG,KAAK,EAAE,CAAsC,EAAE,EAAE;QACnE,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,MAAM,IAAI,YAAY;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;YAC1F,OAAO;QACT,CAAC;QACD,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YACjC,OAAO,EAAE,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;YACpD,OAAO,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACjE,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,KACT,IAAI,4CAEM,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EACrC,QAAQ,EAAE,YAAY,IAAI,MAAM,EAChC,OAAO,EAAE,WAAW,YAEnB,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,GAC1D,CACV,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <CartLineQuantityAdjustButton> — cart 行数量加减按钮
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen <CartLineQuantityAdjustButton>:
|
|
5
|
+
* - +/- 按钮调用 onChange
|
|
6
|
+
* - 防抖(300ms)避免点快导致多次请求
|
|
7
|
+
* - min/max 校验
|
|
8
|
+
* - 移除时弹确认(可选)
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* const { updateLine, removeLine } = useCart();
|
|
12
|
+
* <CartLineQuantityAdjustButton
|
|
13
|
+
* line={line}
|
|
14
|
+
* onUpdate={(newQty) => updateLine(line.id, newQty)}
|
|
15
|
+
* onRemove={() => removeLine(line.id)}
|
|
16
|
+
* />
|
|
17
|
+
*/
|
|
18
|
+
import * as React from 'react';
|
|
19
|
+
export interface CartLineQuantityAdjustButtonProps {
|
|
20
|
+
/** line 的当前数量 */
|
|
21
|
+
quantity: number;
|
|
22
|
+
/** 最小值,默认 1(小于此值改成 remove) */
|
|
23
|
+
min?: number;
|
|
24
|
+
/** 最大值,默认 99 */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** 数量变化回调 */
|
|
27
|
+
onUpdate?: (newQuantity: number) => Promise<void> | void;
|
|
28
|
+
/** 数量降到 0 时触发 remove */
|
|
29
|
+
onRemove?: () => Promise<void> | void;
|
|
30
|
+
/** 防抖延迟(ms),默认 300 */
|
|
31
|
+
debounceMs?: number;
|
|
32
|
+
/** 自定义包装容器 className */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** 减号按钮 props 透传 */
|
|
35
|
+
decreaseProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
36
|
+
/** 加号按钮 props 透传 */
|
|
37
|
+
increaseProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
38
|
+
}
|
|
39
|
+
export declare function CartLineQuantityAdjustButton(props: CartLineQuantityAdjustButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
//# sourceMappingURL=CartLineQuantityAdjustButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartLineQuantityAdjustButton.d.ts","sourceRoot":"","sources":["../../src/components/CartLineQuantityAdjustButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,iCAAiC;IAChD,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa;IACb,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,sBAAsB;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;IAC9D,oBAAoB;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CAC/D;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,iCAAiC,2CA6EpF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* <CartLineQuantityAdjustButton> — cart 行数量加减按钮
|
|
4
|
+
*
|
|
5
|
+
* 对齐 @shopify/hydrogen <CartLineQuantityAdjustButton>:
|
|
6
|
+
* - +/- 按钮调用 onChange
|
|
7
|
+
* - 防抖(300ms)避免点快导致多次请求
|
|
8
|
+
* - min/max 校验
|
|
9
|
+
* - 移除时弹确认(可选)
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* const { updateLine, removeLine } = useCart();
|
|
13
|
+
* <CartLineQuantityAdjustButton
|
|
14
|
+
* line={line}
|
|
15
|
+
* onUpdate={(newQty) => updateLine(line.id, newQty)}
|
|
16
|
+
* onRemove={() => removeLine(line.id)}
|
|
17
|
+
* />
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export function CartLineQuantityAdjustButton(props) {
|
|
21
|
+
const { quantity, min = 1, max = 99, onUpdate, onRemove, debounceMs = 300, className, decreaseProps, increaseProps, } = props;
|
|
22
|
+
// optimistic 显示的 quantity
|
|
23
|
+
const [optimistic, setOptimistic] = React.useState(quantity);
|
|
24
|
+
React.useEffect(() => setOptimistic(quantity), [quantity]);
|
|
25
|
+
const [pending, setPending] = React.useState(false);
|
|
26
|
+
const timerRef = React.useRef(null);
|
|
27
|
+
const flushUpdate = React.useCallback((target) => {
|
|
28
|
+
if (timerRef.current)
|
|
29
|
+
clearTimeout(timerRef.current);
|
|
30
|
+
timerRef.current = setTimeout(async () => {
|
|
31
|
+
setPending(true);
|
|
32
|
+
try {
|
|
33
|
+
if (target < min) {
|
|
34
|
+
await onRemove?.();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
await onUpdate?.(target);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setPending(false);
|
|
42
|
+
}
|
|
43
|
+
}, debounceMs);
|
|
44
|
+
}, [min, onRemove, onUpdate, debounceMs]);
|
|
45
|
+
const handleAdjust = (delta) => {
|
|
46
|
+
const next = optimistic + delta;
|
|
47
|
+
if (next > max)
|
|
48
|
+
return;
|
|
49
|
+
setOptimistic(next);
|
|
50
|
+
flushUpdate(next);
|
|
51
|
+
};
|
|
52
|
+
React.useEffect(() => () => {
|
|
53
|
+
if (timerRef.current)
|
|
54
|
+
clearTimeout(timerRef.current);
|
|
55
|
+
}, []);
|
|
56
|
+
return (_jsxs("div", { className: className, "data-quantity-adjust": true, style: { display: 'inline-flex', alignItems: 'center' }, children: [_jsx("button", { type: "button", ...decreaseProps, "aria-label": "\u51CF\u5C11", "data-quantity-decrease": true, onClick: () => handleAdjust(-1), disabled: pending && optimistic <= min, children: decreaseProps?.children ?? '−' }), _jsx("span", { "data-quantity-value": true, "aria-live": "polite", style: { margin: '0 0.5em', minWidth: '1.5em', textAlign: 'center' }, children: optimistic }), _jsx("button", { type: "button", ...increaseProps, "aria-label": "\u589E\u52A0", "data-quantity-increase": true, onClick: () => handleAdjust(1), disabled: pending && optimistic >= max, children: increaseProps?.children ?? '+' })] }));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=CartLineQuantityAdjustButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartLineQuantityAdjustButton.js","sourceRoot":"","sources":["../../src/components/CartLineQuantityAdjustButton.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Image> — 响应式商品图片
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen 的 <Image>:
|
|
5
|
+
* - 自动生成 srcset(5 个分辨率档:375 / 750 / 1024 / 1536 / 1920)
|
|
6
|
+
* - sizes 媒体查询
|
|
7
|
+
* - loading="lazy" 默认(首屏可关)
|
|
8
|
+
* - decoding="async"
|
|
9
|
+
* - aspectRatio 占位(不抖)
|
|
10
|
+
* - 自动 alt
|
|
11
|
+
*
|
|
12
|
+
* srcset 里的 url 通过 ?width= query 表达,服务端可忽略或接 cdn-cgi/image。
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* <Image data={{ url, altText, width, height }} sizes="(min-width: 768px) 50vw, 100vw" />
|
|
16
|
+
* <Image data={...} aspectRatio="1/1" />
|
|
17
|
+
* <Image data={...} loading="eager" /> // 首屏
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface ImageData {
|
|
21
|
+
url: string;
|
|
22
|
+
altText?: string | null;
|
|
23
|
+
width?: number | null;
|
|
24
|
+
height?: number | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'loading'> {
|
|
27
|
+
/** 必传:图片对象 */
|
|
28
|
+
data: ImageData;
|
|
29
|
+
/** 媒体查询 sizes,例如 (min-width: 768px) 50vw, 100vw */
|
|
30
|
+
sizes?: string;
|
|
31
|
+
/** 强制宽高比,例如 '1/1' '4/3' '16/9' */
|
|
32
|
+
aspectRatio?: string;
|
|
33
|
+
/** lazy(默认) / eager(首屏图用) */
|
|
34
|
+
loading?: 'lazy' | 'eager';
|
|
35
|
+
/** 自定义 srcset 档位 */
|
|
36
|
+
widths?: number[];
|
|
37
|
+
}
|
|
38
|
+
export declare function Image(props: ImageProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=Image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/G,cAAc;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAkBD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,2CAmCtC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// 默认 srcset 档位
|
|
3
|
+
const DEFAULT_WIDTHS = [375, 750, 1024, 1536, 1920];
|
|
4
|
+
/** 给 url 加 ?width=N query */
|
|
5
|
+
function withWidth(url, width) {
|
|
6
|
+
try {
|
|
7
|
+
const u = new URL(url);
|
|
8
|
+
u.searchParams.set('width', String(width));
|
|
9
|
+
return u.toString();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// 不是绝对 URL,按相对路径处理
|
|
13
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
14
|
+
return `${url}${sep}width=${width}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function Image(props) {
|
|
18
|
+
const { data, sizes, aspectRatio, loading = 'lazy', widths = DEFAULT_WIDTHS, style, alt: altProp, ...rest } = props;
|
|
19
|
+
const srcset = widths.map((w) => `${withWidth(data.url, w)} ${w}w`).join(', ');
|
|
20
|
+
// src 用中间档位
|
|
21
|
+
const src = withWidth(data.url, widths[Math.floor(widths.length / 2)]);
|
|
22
|
+
const finalStyle = {
|
|
23
|
+
...(aspectRatio ? { aspectRatio, objectFit: 'cover' } : null),
|
|
24
|
+
...style,
|
|
25
|
+
};
|
|
26
|
+
return (_jsx("img", { ...rest, src: src, srcSet: srcset, sizes: sizes, alt: altProp ?? data.altText ?? '', loading: loading, decoding: "async", width: data.width || undefined, height: data.height || undefined, style: finalStyle }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=Image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Image.js","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":";AAyCA,eAAe;AACf,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEpD,6BAA6B;AAC7B,SAAS,SAAS,CAAC,GAAW,EAAE,KAAa;IAC3C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB;QACnB,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAC1C,OAAO,GAAG,GAAG,GAAG,GAAG,SAAS,KAAK,EAAE,CAAC;IACtC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EACJ,IAAI,EACJ,KAAK,EACL,WAAW,EACX,OAAO,GAAG,MAAM,EAChB,MAAM,GAAG,cAAc,EACvB,KAAK,EACL,GAAG,EAAE,OAAO,EACZ,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/E,YAAY;IACZ,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAwB;QACtC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,OAAgB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,GAAG,KAAK;KACT,CAAC;IAEF,OAAO,CACL,iBACM,IAAI,EACR,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,EAAE,EAClC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAC,OAAO,EAChB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS,EAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,SAAS,EAChC,KAAK,EAAE,UAAU,GACjB,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Money> — 货币格式化
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen 的 <Money>。
|
|
5
|
+
* - 自动按 currencyCode + locale 用 Intl.NumberFormat 格式化
|
|
6
|
+
* - 支持 withoutCurrency / withoutTrailingZeros
|
|
7
|
+
* - 支持 as 渲染成任意元素(默认 <span>)
|
|
8
|
+
* - 支持 measurement(unit price,例如 ¥9.99/100g)
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* <Money data={{ amount: '9.99', currencyCode: 'CNY' }} />
|
|
12
|
+
* <Money data={{ ... }} withoutCurrency />
|
|
13
|
+
* <Money data={{ ... }} as="div" measurement={{ referenceUnit: 'kg', quantity: 0.1 }} />
|
|
14
|
+
*/
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
export interface MoneyData {
|
|
17
|
+
amount: string;
|
|
18
|
+
currencyCode: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MoneyMeasurement {
|
|
21
|
+
/** 比如 'kg' / '100g' / 'l' */
|
|
22
|
+
referenceUnit: string;
|
|
23
|
+
/** 比如 0.1 表示每 100g */
|
|
24
|
+
quantity?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface MoneyProps extends React.HTMLAttributes<HTMLElement> {
|
|
27
|
+
/** 必传:金额对象 */
|
|
28
|
+
data: MoneyData;
|
|
29
|
+
/** 不显示货币符号,只显示数字 */
|
|
30
|
+
withoutCurrency?: boolean;
|
|
31
|
+
/** 整数时不显示 .00 */
|
|
32
|
+
withoutTrailingZeros?: boolean;
|
|
33
|
+
/** locale 字符串(zh-CN / en-US / ...)。默认按 currencyCode 推断 */
|
|
34
|
+
locale?: string;
|
|
35
|
+
/** 渲染元素,默认 span */
|
|
36
|
+
as?: keyof JSX.IntrinsicElements;
|
|
37
|
+
/** unit price:渲染 ¥99.00/100g */
|
|
38
|
+
measurement?: MoneyMeasurement;
|
|
39
|
+
}
|
|
40
|
+
export declare function Money(props: MoneyProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
//# sourceMappingURL=Money.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Money.d.ts","sourceRoot":"","sources":["../../src/components/Money.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IACnE,cAAc;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,oBAAoB;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB;IACjB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB;IACnB,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,iBAAiB,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAYD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,2CAmDtC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// currencyCode → 默认 locale 推断
|
|
3
|
+
const DEFAULT_LOCALE_BY_CURRENCY = {
|
|
4
|
+
CNY: 'zh-CN',
|
|
5
|
+
USD: 'en-US',
|
|
6
|
+
EUR: 'de-DE',
|
|
7
|
+
GBP: 'en-GB',
|
|
8
|
+
JPY: 'ja-JP',
|
|
9
|
+
KRW: 'ko-KR',
|
|
10
|
+
};
|
|
11
|
+
export function Money(props) {
|
|
12
|
+
const { data, withoutCurrency, withoutTrailingZeros, locale: localeProp, as, measurement, ...rest } = props;
|
|
13
|
+
const Tag = as || 'span';
|
|
14
|
+
const locale = localeProp || DEFAULT_LOCALE_BY_CURRENCY[data.currencyCode] || 'en-US';
|
|
15
|
+
const amount = Number(data.amount);
|
|
16
|
+
let formatted;
|
|
17
|
+
try {
|
|
18
|
+
const opts = withoutCurrency
|
|
19
|
+
? {
|
|
20
|
+
minimumFractionDigits: withoutTrailingZeros && Number.isInteger(amount) ? 0 : 2,
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
style: 'currency',
|
|
25
|
+
currency: data.currencyCode,
|
|
26
|
+
minimumFractionDigits: withoutTrailingZeros && Number.isInteger(amount) ? 0 : 2,
|
|
27
|
+
maximumFractionDigits: 2,
|
|
28
|
+
};
|
|
29
|
+
formatted = new Intl.NumberFormat(locale, opts).format(amount);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Fallback:Intl 不支持的 currencyCode
|
|
33
|
+
formatted = withoutCurrency ? amount.toFixed(2) : `${data.currencyCode} ${amount.toFixed(2)}`;
|
|
34
|
+
}
|
|
35
|
+
// measurement suffix(unit price)
|
|
36
|
+
let suffix = '';
|
|
37
|
+
if (measurement) {
|
|
38
|
+
const q = measurement.quantity ?? 1;
|
|
39
|
+
if (q === 1) {
|
|
40
|
+
suffix = `/${measurement.referenceUnit}`;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
suffix = `/${q}${measurement.referenceUnit}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return (_jsxs(Tag, { ...rest, "data-money": data.amount, "data-money-currency": data.currencyCode, children: [formatted, suffix] }));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=Money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Money.js","sourceRoot":"","sources":["../../src/components/Money.tsx"],"names":[],"mappings":";AA4CA,8BAA8B;AAC9B,MAAM,0BAA0B,GAA2B;IACzD,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;CACb,CAAC;AAEF,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EACJ,IAAI,EACJ,eAAe,EACf,oBAAoB,EACpB,MAAM,EAAE,UAAU,EAClB,EAAE,EACF,WAAW,EACX,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,GAAG,GAAQ,EAAE,IAAI,MAAM,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,IAAI,0BAA0B,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC;IACtF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEnC,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,IAAI,GAA6B,eAAe;YACpD,CAAC,CAAC;gBACE,qBAAqB,EAAE,oBAAoB,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/E,qBAAqB,EAAE,CAAC;aACzB;YACH,CAAC,CAAC;gBACE,KAAK,EAAE,UAAU;gBACjB,QAAQ,EAAE,IAAI,CAAC,YAAY;gBAC3B,qBAAqB,EAAE,oBAAoB,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/E,qBAAqB,EAAE,CAAC;aACzB,CAAC;QACN,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;QAClC,SAAS,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAChG,CAAC;IAED,iCAAiC;IACjC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,CAAC,GAAG,WAAW,CAAC,QAAQ,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,WAAW,CAAC,aAAa,EAAE,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC,aAAa,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,OAAO,CACL,MAAC,GAAG,OAAK,IAAI,gBAAc,IAAI,CAAC,MAAM,yBAAuB,IAAI,CAAC,YAAY,aAC3E,SAAS,EACT,MAAM,IACH,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <ProductPrice> — 商品价格
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen <ProductPrice>:
|
|
5
|
+
* - 渲染当前价
|
|
6
|
+
* - 当 compareAtPrice > price 时自动显示划线对比价
|
|
7
|
+
* - 支持 unit price 模式
|
|
8
|
+
* - 可定制 className / 容器
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* <ProductPrice price={{ amount: '99', currencyCode: 'CNY' }} />
|
|
12
|
+
* <ProductPrice price={...} compareAtPrice={{ amount: '129', ... }} />
|
|
13
|
+
*/
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
import { type MoneyData, type MoneyMeasurement } from './Money';
|
|
16
|
+
export interface ProductPriceProps extends React.HTMLAttributes<HTMLElement> {
|
|
17
|
+
price: MoneyData;
|
|
18
|
+
compareAtPrice?: MoneyData | null;
|
|
19
|
+
/** unit price,例如每 100g */
|
|
20
|
+
measurement?: MoneyMeasurement;
|
|
21
|
+
/** locale 透传 */
|
|
22
|
+
locale?: string;
|
|
23
|
+
/** 划线价的额外 className */
|
|
24
|
+
compareAtClassName?: string;
|
|
25
|
+
as?: keyof JSX.IntrinsicElements;
|
|
26
|
+
}
|
|
27
|
+
export declare function ProductPrice(props: ProductPriceProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
//# sourceMappingURL=ProductPrice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductPrice.d.ts","sourceRoot":"","sources":["../../src/components/ProductPrice.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IAC1E,KAAK,EAAE,SAAS,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,0BAA0B;IAC1B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,gBAAgB;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,iBAAiB,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,2CA+BpD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Money } from './Money';
|
|
3
|
+
export function ProductPrice(props) {
|
|
4
|
+
const { price, compareAtPrice, measurement, locale, compareAtClassName, as, ...rest } = props;
|
|
5
|
+
const Tag = as || 'div';
|
|
6
|
+
const hasDiscount = compareAtPrice &&
|
|
7
|
+
Number(compareAtPrice.amount) > Number(price.amount);
|
|
8
|
+
return (_jsxs(Tag, { ...rest, "data-product-price": true, children: [_jsx(Money, { data: price, locale: locale, measurement: measurement }), hasDiscount && compareAtPrice && (_jsx(Money, { data: compareAtPrice, locale: locale, as: "s", className: compareAtClassName, "data-compare-at": "true", style: { marginLeft: '0.5em', opacity: 0.6 } }))] }));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ProductPrice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductPrice.js","sourceRoot":"","sources":["../../src/components/ProductPrice.tsx"],"names":[],"mappings":";AAeA,OAAO,EAAE,KAAK,EAAyC,MAAM,SAAS,CAAC;AAcvE,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EACJ,KAAK,EACL,cAAc,EACd,WAAW,EACX,MAAM,EACN,kBAAkB,EAClB,EAAE,EACF,GAAG,IAAI,EACR,GAAG,KAAK,CAAC;IAEV,MAAM,GAAG,GAAQ,EAAE,IAAI,KAAK,CAAC;IAC7B,MAAM,WAAW,GACf,cAAc;QACd,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEvD,OAAO,CACL,MAAC,GAAG,OAAK,IAAI,yCACX,KAAC,KAAK,IAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAI,EAC/D,WAAW,IAAI,cAAc,IAAI,CAChC,KAAC,KAAK,IACJ,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,EAAE,EAAC,GAAG,EACN,SAAS,EAAE,kBAAkB,qBACb,MAAM,EACtB,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAC5C,CACH,IACG,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <VariantSelector> — 商品 variant 选择器
|
|
3
|
+
*
|
|
4
|
+
* 对齐 @shopify/hydrogen <VariantSelector>:
|
|
5
|
+
* - 自动解析 variants 的 selectedOptions 维度(颜色 / 尺寸 等)
|
|
6
|
+
* - 渲染每个维度的选项按钮
|
|
7
|
+
* - 不可购买组合 disabled
|
|
8
|
+
* - URL 同步(可选)
|
|
9
|
+
* - 默认选第一个 availableForSale
|
|
10
|
+
*
|
|
11
|
+
* 设计:组件用 render prop,把每个 option 的选项交给商家渲染。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* <VariantSelector
|
|
15
|
+
* product={product}
|
|
16
|
+
* value={selectedVariantId}
|
|
17
|
+
* onChange={setSelectedVariantId}
|
|
18
|
+
* >
|
|
19
|
+
* {({ option, value, onSelect }) => (
|
|
20
|
+
* <div>
|
|
21
|
+
* <h4>{option.name}</h4>
|
|
22
|
+
* {option.values.map((v) => (
|
|
23
|
+
* <button
|
|
24
|
+
* key={v.value}
|
|
25
|
+
* disabled={!v.available}
|
|
26
|
+
* onClick={() => onSelect(v.value)}
|
|
27
|
+
* aria-pressed={v.value === value}
|
|
28
|
+
* >
|
|
29
|
+
* {v.value}
|
|
30
|
+
* </button>
|
|
31
|
+
* ))}
|
|
32
|
+
* </div>
|
|
33
|
+
* )}
|
|
34
|
+
* </VariantSelector>
|
|
35
|
+
*/
|
|
36
|
+
import * as React from 'react';
|
|
37
|
+
interface SelectedOption {
|
|
38
|
+
name: string;
|
|
39
|
+
value: string;
|
|
40
|
+
}
|
|
41
|
+
interface ProductVariant {
|
|
42
|
+
id: string;
|
|
43
|
+
availableForSale: boolean;
|
|
44
|
+
selectedOptions?: SelectedOption[];
|
|
45
|
+
}
|
|
46
|
+
interface ProductLike {
|
|
47
|
+
options?: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
values: string[];
|
|
50
|
+
}>;
|
|
51
|
+
variants: {
|
|
52
|
+
nodes: ProductVariant[];
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export interface VariantOption {
|
|
56
|
+
name: string;
|
|
57
|
+
values: Array<{
|
|
58
|
+
value: string;
|
|
59
|
+
available: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
export interface VariantSelectorRenderProps {
|
|
63
|
+
option: VariantOption;
|
|
64
|
+
/** 当前已选择的 value(在这个 option 维度上) */
|
|
65
|
+
value: string | undefined;
|
|
66
|
+
/** 选择 value,触发 onChange 更新 selectedVariantId */
|
|
67
|
+
onSelect: (value: string) => void;
|
|
68
|
+
}
|
|
69
|
+
export interface VariantSelectorProps {
|
|
70
|
+
product: ProductLike;
|
|
71
|
+
/** 当前选中 variant 的 GID */
|
|
72
|
+
value: string;
|
|
73
|
+
/** variant 切换时调用 */
|
|
74
|
+
onChange?: (variantId: string) => void;
|
|
75
|
+
/** 每个 option 的渲染函数 */
|
|
76
|
+
children: (renderProps: VariantSelectorRenderProps) => React.ReactNode;
|
|
77
|
+
}
|
|
78
|
+
export declare function VariantSelector(props: VariantSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
79
|
+
export {};
|
|
80
|
+
//# sourceMappingURL=VariantSelector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VariantSelector.d.ts","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACpD,QAAQ,EAAE;QAAE,KAAK,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;IACtB,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,WAAW,CAAC;IACrB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,sBAAsB;IACtB,QAAQ,EAAE,CAAC,WAAW,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAC;CACxE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAsE1D"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* <VariantSelector> — 商品 variant 选择器
|
|
4
|
+
*
|
|
5
|
+
* 对齐 @shopify/hydrogen <VariantSelector>:
|
|
6
|
+
* - 自动解析 variants 的 selectedOptions 维度(颜色 / 尺寸 等)
|
|
7
|
+
* - 渲染每个维度的选项按钮
|
|
8
|
+
* - 不可购买组合 disabled
|
|
9
|
+
* - URL 同步(可选)
|
|
10
|
+
* - 默认选第一个 availableForSale
|
|
11
|
+
*
|
|
12
|
+
* 设计:组件用 render prop,把每个 option 的选项交给商家渲染。
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* <VariantSelector
|
|
16
|
+
* product={product}
|
|
17
|
+
* value={selectedVariantId}
|
|
18
|
+
* onChange={setSelectedVariantId}
|
|
19
|
+
* >
|
|
20
|
+
* {({ option, value, onSelect }) => (
|
|
21
|
+
* <div>
|
|
22
|
+
* <h4>{option.name}</h4>
|
|
23
|
+
* {option.values.map((v) => (
|
|
24
|
+
* <button
|
|
25
|
+
* key={v.value}
|
|
26
|
+
* disabled={!v.available}
|
|
27
|
+
* onClick={() => onSelect(v.value)}
|
|
28
|
+
* aria-pressed={v.value === value}
|
|
29
|
+
* >
|
|
30
|
+
* {v.value}
|
|
31
|
+
* </button>
|
|
32
|
+
* ))}
|
|
33
|
+
* </div>
|
|
34
|
+
* )}
|
|
35
|
+
* </VariantSelector>
|
|
36
|
+
*/
|
|
37
|
+
import * as React from 'react';
|
|
38
|
+
export function VariantSelector(props) {
|
|
39
|
+
const { product, value, onChange, children } = props;
|
|
40
|
+
const variants = product.variants.nodes;
|
|
41
|
+
const currentVariant = variants.find((v) => v.id === value) || variants[0];
|
|
42
|
+
// 推断 options(如果 product.options 没传,从 variants.selectedOptions 提取)
|
|
43
|
+
const options = React.useMemo(() => {
|
|
44
|
+
if (product.options && product.options.length > 0) {
|
|
45
|
+
return product.options.map((opt) => ({
|
|
46
|
+
name: opt.name,
|
|
47
|
+
values: opt.values.map((v) => ({
|
|
48
|
+
value: v,
|
|
49
|
+
available: variants.some((variant) => variant.availableForSale &&
|
|
50
|
+
variant.selectedOptions?.some((so) => so.name === opt.name && so.value === v)),
|
|
51
|
+
})),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
// 从 variants.selectedOptions 反推
|
|
55
|
+
const dimMap = new Map();
|
|
56
|
+
for (const v of variants) {
|
|
57
|
+
for (const so of v.selectedOptions ?? []) {
|
|
58
|
+
if (!dimMap.has(so.name))
|
|
59
|
+
dimMap.set(so.name, new Set());
|
|
60
|
+
dimMap.get(so.name).add(so.value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Array.from(dimMap.entries()).map(([name, values]) => ({
|
|
64
|
+
name,
|
|
65
|
+
values: Array.from(values).map((v) => ({
|
|
66
|
+
value: v,
|
|
67
|
+
available: variants.some((variant) => variant.availableForSale &&
|
|
68
|
+
variant.selectedOptions?.some((so) => so.name === name && so.value === v)),
|
|
69
|
+
})),
|
|
70
|
+
}));
|
|
71
|
+
}, [product, variants]);
|
|
72
|
+
// 切换某 option 的 value:找一个匹配的 variant
|
|
73
|
+
const handleSelect = (optionName, optionValue) => {
|
|
74
|
+
const currentSelected = new Map((currentVariant?.selectedOptions ?? []).map((so) => [so.name, so.value]));
|
|
75
|
+
currentSelected.set(optionName, optionValue);
|
|
76
|
+
// 找最匹配 + available 的 variant
|
|
77
|
+
const target = variants.find((v) => Array.from(currentSelected.entries()).every(([n, val]) => v.selectedOptions?.some((so) => so.name === n && so.value === val)));
|
|
78
|
+
if (target)
|
|
79
|
+
onChange?.(target.id);
|
|
80
|
+
};
|
|
81
|
+
return (_jsx(_Fragment, { children: options.map((option) => (_jsx(React.Fragment, { children: children({
|
|
82
|
+
option,
|
|
83
|
+
value: currentVariant?.selectedOptions?.find((so) => so.name === option.name)?.value,
|
|
84
|
+
onSelect: (val) => handleSelect(option.name, val),
|
|
85
|
+
}) }, option.name))) }));
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=VariantSelector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VariantSelector.js","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"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"}
|