@shopbb/helium 0.4.0 → 0.5.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * <DiscountSelector>
3
+ *
4
+ * Cart 上的优惠券选择器 — 卡片式 UI,对齐拼多多 / 京东体验。
5
+ *
6
+ * 行为:
7
+ * - 默认显示当前应用的券(服务端自动选了最佳,或买家显式选的)
8
+ * - 点"切换"打开抽屉/弹窗,列出该买家所有可用券(从 useDiscounts().myDiscounts)
9
+ * - 选另一张 → 调 cartDiscountSelect mutation
10
+ * - 点"不使用优惠券" → cartDiscountClear
11
+ *
12
+ * 未登录买家:渲染"登录后享受优惠"提示。
13
+ *
14
+ * 设计目标:
15
+ * - 这是 helium "脚手架" 的卡片式实现。商家可以替换为抽屉式 / 弹窗式自定义 UI。
16
+ * - 商家也可以直接用 useDiscounts() 自己渲染(提供 hook-only 路径)。
17
+ */
18
+ import * as React from 'react';
19
+ export interface DiscountSelectorProps {
20
+ className?: string;
21
+ /** 未登录时显示的内容;不传则显示默认 */
22
+ unauthenticatedFallback?: React.ReactNode;
23
+ /** 自定义渲染 */
24
+ renderApplied?: (props: {
25
+ code: string | null;
26
+ title: string;
27
+ amount: {
28
+ amount: string;
29
+ currencyCode: string;
30
+ };
31
+ onChange: () => void;
32
+ onClear: () => void;
33
+ }) => React.ReactNode;
34
+ }
35
+ export declare function DiscountSelector(props: DiscountSelectorProps): import("react/jsx-runtime").JSX.Element;
36
+ //# sourceMappingURL=DiscountSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscountSelector.d.ts","sourceRoot":"","sources":["../../src/components/DiscountSelector.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,uBAAuB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1C,YAAY;IACZ,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,KAAK,CAAC,SAAS,CAAC;CACjL;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,2CA6G5D"}
@@ -0,0 +1,94 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * <DiscountSelector>
4
+ *
5
+ * Cart 上的优惠券选择器 — 卡片式 UI,对齐拼多多 / 京东体验。
6
+ *
7
+ * 行为:
8
+ * - 默认显示当前应用的券(服务端自动选了最佳,或买家显式选的)
9
+ * - 点"切换"打开抽屉/弹窗,列出该买家所有可用券(从 useDiscounts().myDiscounts)
10
+ * - 选另一张 → 调 cartDiscountSelect mutation
11
+ * - 点"不使用优惠券" → cartDiscountClear
12
+ *
13
+ * 未登录买家:渲染"登录后享受优惠"提示。
14
+ *
15
+ * 设计目标:
16
+ * - 这是 helium "脚手架" 的卡片式实现。商家可以替换为抽屉式 / 弹窗式自定义 UI。
17
+ * - 商家也可以直接用 useDiscounts() 自己渲染(提供 hook-only 路径)。
18
+ */
19
+ import * as React from 'react';
20
+ import { useDiscounts } from './DiscountProvider';
21
+ import { useAnalytics } from './AnalyticsProvider';
22
+ import { Money } from './Money';
23
+ export function DiscountSelector(props) {
24
+ const { className, unauthenticatedFallback, renderApplied } = props;
25
+ const { myDiscounts, myDiscountsStatus, cartAllocations, appliedClaim, selectCartDiscount, clearCartDiscount, } = useDiscounts();
26
+ const analytics = useAnalytics();
27
+ const [open, setOpen] = React.useState(false);
28
+ const [pending, setPending] = React.useState(false);
29
+ // 服务端已经决定了 cart 上是哪张券
30
+ const appliedAlloc = cartAllocations[0];
31
+ const appliedClaimId = appliedClaim?.claimId ?? null;
32
+ const appliedTitle = appliedClaim?.title ?? appliedAlloc?.title;
33
+ const appliedCode = appliedClaim?.code ?? appliedAlloc?.code;
34
+ const handleSelect = async (claimId) => {
35
+ setPending(true);
36
+ try {
37
+ await selectCartDiscount(claimId);
38
+ analytics.emit('discount_select', { claimId });
39
+ }
40
+ finally {
41
+ setPending(false);
42
+ setOpen(false);
43
+ }
44
+ };
45
+ const handleClear = async () => {
46
+ setPending(true);
47
+ try {
48
+ await clearCartDiscount();
49
+ }
50
+ finally {
51
+ setPending(false);
52
+ setOpen(false);
53
+ }
54
+ };
55
+ // 未登录
56
+ if (myDiscountsStatus === 'unauthenticated') {
57
+ return unauthenticatedFallback !== undefined ? _jsx(_Fragment, { children: unauthenticatedFallback }) : (_jsxs("div", { "data-discount-selector": true, "data-unauth": true, className: className, children: [_jsx("span", { children: "\u767B\u5F55\u540E\u53EF\u4F7F\u7528\u4F18\u60E0\u5238" }), _jsx("a", { href: "/login?next=/cart", children: "\u53BB\u767B\u5F55" })] }));
58
+ }
59
+ if (myDiscountsStatus === 'loading') {
60
+ return _jsx("div", { "data-discount-selector": true, "data-loading": true, className: className, children: "\u52A0\u8F7D\u4F18\u60E0\u5238\u2026" });
61
+ }
62
+ const availableClaims = myDiscounts.filter((c) => !c.isExpired && c.remainingUses > 0);
63
+ if (availableClaims.length === 0) {
64
+ return (_jsx("div", { "data-discount-selector": true, "data-no-discount": true, className: className, children: _jsx("span", { children: "\u6682\u65E0\u53EF\u7528\u4F18\u60E0\u5238" }) }));
65
+ }
66
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-discount-selector": true, className: className, children: appliedClaimId ? (renderApplied ? renderApplied({
67
+ code: appliedCode ?? null,
68
+ title: appliedTitle ?? '',
69
+ amount: appliedAlloc?.discountedAmount ?? { amount: '0', currencyCode: 'CNY' },
70
+ onChange: () => setOpen(true),
71
+ onClear: handleClear,
72
+ }) : (_jsxs("div", { "data-applied": true, children: [_jsxs("div", { "data-info": true, children: [_jsx("span", { "data-label": true, children: "\u5DF2\u4F7F\u7528" }), _jsx("strong", { "data-title": true, children: appliedTitle }), appliedAlloc && (_jsxs("span", { "data-amount": true, children: ["\u2212 ", _jsx(Money, { data: { amount: appliedAlloc.discountedAmount.amount, currencyCode: appliedAlloc.discountedAmount.currencyCode } })] }))] }), _jsxs("div", { "data-actions": true, children: [_jsx("button", { type: "button", onClick: () => setOpen(true), disabled: pending, children: "\u5207\u6362" }), _jsx("button", { type: "button", onClick: handleClear, disabled: pending, "data-clear": true, children: "\u4E0D\u4F7F\u7528" })] })] }))) : (_jsxs("div", { "data-empty-applied": true, children: [_jsx("div", { "data-info": true, children: _jsxs("span", { "data-label": true, children: ["\u6709 ", availableClaims.length, " \u5F20\u53EF\u7528\u4F18\u60E0\u5238"] }) }), _jsx("button", { type: "button", onClick: () => setOpen(true), disabled: pending, children: "\u9009\u62E9" })] })) }), open && (_jsx(DiscountPickerSheet, { claims: availableClaims, currentClaimId: appliedClaimId, onSelect: handleSelect, onClose: () => setOpen(false), pending: pending }))] }));
73
+ }
74
+ /**
75
+ * 抽屉式选券面板。
76
+ * Demo 实现为屏底固定面板;商家可以替换为 modal / 抽屉等。
77
+ */
78
+ function DiscountPickerSheet({ claims, currentClaimId, onSelect, onClose, pending, }) {
79
+ return (_jsx("div", { "data-discount-picker-overlay": true, onClick: onClose, children: _jsxs("div", { "data-discount-picker-sheet": true, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { "data-sheet-header": true, children: [_jsx("h3", { children: "\u9009\u62E9\u4F18\u60E0\u5238" }), _jsx("button", { type: "button", onClick: onClose, "data-sheet-close": true, children: "\u00D7" })] }), _jsx("div", { "data-sheet-list": true, children: claims.map((c) => {
80
+ const id = c.id.replace(/^gid:\/\/shopbb\/DiscountClaim\//, '');
81
+ const isCurrent = currentClaimId === id;
82
+ return (_jsxs("button", { type: "button", "data-claim-card": true, "data-current": isCurrent ? '' : undefined, onClick: () => onSelect(id), disabled: pending, children: [_jsxs("div", { "data-claim-left": true, children: [_jsx("div", { "data-claim-value": true, children: formatValue(c) }), c.discount.minSubtotal && (_jsxs("div", { "data-claim-min": true, children: ["\u6EE1 ", _jsx(Money, { data: { amount: c.discount.minSubtotal.amount, currencyCode: c.discount.minSubtotal.currencyCode } }), " \u53EF\u7528"] }))] }), _jsxs("div", { "data-claim-right": true, children: [_jsx("div", { "data-claim-title": true, children: c.discount.title }), c.discount.code && _jsx("div", { "data-claim-code": true, children: c.discount.code }), c.expiresAt && (_jsxs("div", { "data-claim-expiry": true, children: [new Date(c.expiresAt).toLocaleDateString('zh-CN'), " \u8FC7\u671F"] }))] }), isCurrent && _jsx("span", { "data-claim-current-tag": true, children: "\u5F53\u524D\u4F7F\u7528" })] }, c.id));
83
+ }) })] }) }));
84
+ }
85
+ function formatValue(c) {
86
+ if (c.discount.valueType === 'PERCENTAGE' && c.discount.valuePercentage != null) {
87
+ return `${100 - c.discount.valuePercentage} 折`;
88
+ }
89
+ if (c.discount.valueType === 'FIXED_AMOUNT' && c.discount.valueAmount) {
90
+ return `− ¥${c.discount.valueAmount.amount}`;
91
+ }
92
+ return '免运费';
93
+ }
94
+ //# sourceMappingURL=DiscountSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscountSelector.js","sourceRoot":"","sources":["../../src/components/DiscountSelector.tsx"],"names":[],"mappings":"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"}
@@ -48,6 +48,8 @@ export { AddressPicker } from './AddressPicker';
48
48
  export type { AddressPickerProps } from './AddressPicker';
49
49
  export { DiscountProvider, useDiscounts, useDiscountsOptional, useProductDiscounts, } from './DiscountProvider';
50
50
  export type { Discount, DiscountClaim, DiscountValueType, DiscountValuePercentage, DiscountValueAmount, DiscountValueFreeShipping, CartDiscountCode, CartDiscountAllocation, DiscountUserError, DiscountContextValue, DiscountProviderProps, } from './DiscountProvider';
51
- export { DiscountCodeInput, AppliedDiscountList, BestDiscountHint, ClaimableDiscountList, DiscountClaimButton, MyDiscountList, } from './DiscountComponents';
52
- export type { DiscountCodeInputProps, AppliedDiscountListProps, BestDiscountHintProps, ClaimableDiscountListProps, DiscountClaimButtonProps, MyDiscountListProps, } from './DiscountComponents';
51
+ export { AppliedDiscountList, ClaimableDiscountList, DiscountClaimButton, MyDiscountList, } from './DiscountComponents';
52
+ export type { AppliedDiscountListProps, ClaimableDiscountListProps, DiscountClaimButtonProps, MyDiscountListProps, } from './DiscountComponents';
53
+ export { DiscountSelector } from './DiscountSelector';
54
+ export type { DiscountSelectorProps } from './DiscountSelector';
53
55
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"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"}
@@ -37,5 +37,6 @@ export { AddressForm } from './AddressForm';
37
37
  export { AddressPicker } from './AddressPicker';
38
38
  // 优惠券(W4b)
39
39
  export { DiscountProvider, useDiscounts, useDiscountsOptional, useProductDiscounts, } from './DiscountProvider';
40
- export { DiscountCodeInput, AppliedDiscountList, BestDiscountHint, ClaimableDiscountList, DiscountClaimButton, MyDiscountList, } from './DiscountComponents';
40
+ export { AppliedDiscountList, ClaimableDiscountList, DiscountClaimButton, MyDiscountList, } from './DiscountComponents';
41
+ export { DiscountSelector } from './DiscountSelector';
41
42
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,oBAAoB;AACpB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGxE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAWxE,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGjF,QAAQ;AACR,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAG9E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAOpD,WAAW;AACX,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAU/B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,WAAW;AACX,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAe5B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,oBAAoB;AACpB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGxE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAWxE,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGjF,QAAQ;AACR,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAG9E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAOpD,WAAW;AACX,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAU/B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,WAAW;AACX,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAe5B,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbb/helium",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -101,8 +101,17 @@ const CART_FRAGMENT = /* GraphQL */ `
101
101
  }
102
102
  cost {
103
103
  subtotalAmount { amount currencyCode }
104
+ totalDiscountAmount { amount currencyCode }
104
105
  totalAmount { amount currencyCode }
105
106
  }
107
+ discountCodes { code applicable }
108
+ discountAllocations {
109
+ discountedAmount { amount currencyCode }
110
+ targetType
111
+ targetId
112
+ title
113
+ code
114
+ }
106
115
  }
107
116
  `;
108
117
 
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Discount UI 组件集(W4b)
2
+ * Discount UI 组件集
3
3
  *
4
- * <DiscountCodeInput> cart / checkout 输入框
5
- * <AppliedDiscountList> 当前 cart 已应用的码列表
6
- * <BestDiscountHint> "可省 ¥X" 提示
4
+ * <AppliedDiscountList> 当前 cart 已应用的折扣分配(只读)
7
5
  * <ClaimableDiscountList> 可领取的优惠券列表(首页 / 商品页)
8
6
  * <DiscountClaimButton> 单个领取按钮
9
7
  * <MyDiscountList> 我的卡包
10
8
  *
9
+ * 注:W5 已删除以下组件(旧设计):
10
+ * - <DiscountCodeInput>: 输入框范式已过时。请用 <DiscountSelector> + <ClaimableDiscountList>。
11
+ * - <BestDiscountHint>: 服务端自动选择最佳券,不需要前端提示。用户在 <DiscountSelector> 看到 / 切换。
12
+ *
11
13
  * 全部无样式 + data-* 钩子。
12
14
  */
13
15
 
@@ -17,70 +19,7 @@ import { useAnalytics } from './AnalyticsProvider';
17
19
  import { Money } from './Money';
18
20
 
19
21
  // ============================================================
20
- // 1. DiscountCodeInput
21
- // ============================================================
22
-
23
- export interface DiscountCodeInputProps {
24
- placeholder?: string;
25
- buttonText?: string;
26
- onApplied?: (code: string) => void;
27
- onError?: (msg: string) => void;
28
- className?: string;
29
- }
30
-
31
- export function DiscountCodeInput(props: DiscountCodeInputProps) {
32
- const { placeholder = '输入优惠码', buttonText = '应用', onApplied, onError, className } = props;
33
- const { applyToCart } = useDiscounts();
34
- const analytics = useAnalytics();
35
- const [code, setCode] = React.useState('');
36
- const [loading, setLoading] = React.useState(false);
37
- const [err, setErr] = React.useState<string | null>(null);
38
-
39
- const submit = async () => {
40
- const c = code.trim();
41
- if (!c) return;
42
- setLoading(true);
43
- setErr(null);
44
- try {
45
- const r = await applyToCart(c);
46
- if (r.userErrors.length > 0) {
47
- const msg = r.userErrors[0].message;
48
- setErr(msg);
49
- onError?.(msg);
50
- } else {
51
- setCode('');
52
- onApplied?.(c);
53
- analytics.emit('discount_apply_to_cart', { code: c });
54
- }
55
- } finally {
56
- setLoading(false);
57
- }
58
- };
59
-
60
- return (
61
- <form
62
- data-discount-code-input
63
- className={className}
64
- onSubmit={(e) => { e.preventDefault(); submit(); }}
65
- >
66
- <div data-row>
67
- <input
68
- value={code}
69
- onChange={(e) => setCode(e.target.value)}
70
- placeholder={placeholder}
71
- disabled={loading}
72
- />
73
- <button type="submit" disabled={loading || !code.trim()}>
74
- {loading ? '...' : buttonText}
75
- </button>
76
- </div>
77
- {err && <div data-error>{err}</div>}
78
- </form>
79
- );
80
- }
81
-
82
- // ============================================================
83
- // 2. AppliedDiscountList
22
+ // AppliedDiscountList
84
23
  // ============================================================
85
24
 
86
25
  export interface AppliedDiscountListProps {
@@ -90,96 +29,37 @@ export interface AppliedDiscountListProps {
90
29
 
91
30
  export function AppliedDiscountList(props: AppliedDiscountListProps) {
92
31
  const { className, emptyText = null } = props;
93
- const { appliedToCart, cartAllocations, removeFromCart } = useDiscounts();
94
- const analytics = useAnalytics();
32
+ const { cartAllocations } = useDiscounts();
95
33
 
96
- if (appliedToCart.length === 0) return <>{emptyText}</>;
34
+ if (cartAllocations.length === 0) return <>{emptyText}</>;
97
35
 
98
36
  return (
99
37
  <div data-applied-discount-list className={className}>
100
- {appliedToCart.map((dc) => {
101
- const alloc = cartAllocations.find((a) => a.code === dc.code);
102
- return (
103
- <div
104
- key={dc.code}
105
- data-applied-item
106
- data-applicable={dc.applicable ? '' : undefined}
107
- >
108
- <div data-info>
109
- <span data-code>{dc.code}</span>
110
- {alloc && (
111
- <span data-discount-amount>
112
- − <Money data={{ amount: alloc.discountedAmount.amount, currencyCode: alloc.discountedAmount.currencyCode }} />
113
- </span>
114
- )}
115
- {!dc.applicable && <span data-warning>不适用</span>}
116
- </div>
117
- <button
118
- type="button"
119
- data-remove
120
- onClick={async () => {
121
- await removeFromCart(dc.code);
122
- analytics.emit('discount_remove_from_cart', { code: dc.code });
123
- }}
124
- >
125
- ×
126
- </button>
38
+ {cartAllocations.map((alloc, i) => (
39
+ <div key={alloc.code || i} data-applied-item>
40
+ <div data-info>
41
+ <span data-title>{alloc.title}</span>
42
+ {alloc.code && <span data-code>{alloc.code}</span>}
127
43
  </div>
128
- );
129
- })}
130
- </div>
131
- );
132
- }
133
-
134
- // ============================================================
135
- // 3. BestDiscountHint
136
- // ============================================================
137
-
138
- export interface BestDiscountHintProps {
139
- className?: string;
140
- /** 自定义渲染(高级用法) */
141
- render?: (best: { discount: DiscountClaim; estimatedAmount: number; apply: () => Promise<void> }) => React.ReactNode;
142
- }
143
-
144
- export function BestDiscountHint(props: BestDiscountHintProps) {
145
- const { className, render } = props;
146
- const { bestApplicableForCart, applyToCart } = useDiscounts();
147
- const analytics = useAnalytics();
148
- if (!bestApplicableForCart) return null;
149
- const { discount: claim, estimatedAmount } = bestApplicableForCart;
150
- const code = claim.discount.code;
151
- if (!code) return null;
152
-
153
- const apply = async () => {
154
- await applyToCart(code);
155
- analytics.emit('discount_apply_to_cart', { code, source: 'best_hint' });
156
- };
157
-
158
- if (render) return <>{render({ discount: claim, estimatedAmount, apply })}</>;
159
-
160
- return (
161
- <div data-best-discount-hint className={className}>
162
- <div data-msg>
163
- 使用 <strong data-code>{code}</strong> 可省{' '}
164
- <strong data-amount><Money data={{ amount: String(estimatedAmount), currencyCode: 'CNY' }} /></strong>
165
- </div>
166
- <button type="button" onClick={apply} data-apply>立即使用</button>
44
+ <span data-discount-amount>
45
+ − <Money data={{ amount: alloc.discountedAmount.amount, currencyCode: alloc.discountedAmount.currencyCode }} />
46
+ </span>
47
+ </div>
48
+ ))}
167
49
  </div>
168
50
  );
169
51
  }
170
52
 
171
53
  // ============================================================
172
- // 4. ClaimableDiscountList
54
+ // ClaimableDiscountList
173
55
  // ============================================================
174
56
 
175
57
  export interface ClaimableDiscountListProps {
176
58
  scope?: 'store' | 'product';
177
- /** scope='product' 时必传 */
178
59
  productHandle?: string;
179
60
  first?: number;
180
61
  className?: string;
181
62
  emptyText?: React.ReactNode;
182
- /** 自定义渲染(高级用法) */
183
63
  renderItem?: (discount: Discount) => React.ReactNode;
184
64
  }
185
65
 
@@ -226,17 +106,13 @@ function DefaultClaimableItem({ discount: d }: { discount: Discount }) {
226
106
  }
227
107
 
228
108
  function formatDiscountValue(d: Discount): string {
229
- if (d.value.__typename === 'DiscountPercentage') {
230
- return `${100 - d.value.percentage} 折`;
231
- }
232
- if (d.value.__typename === 'DiscountAmount') {
233
- return `减 ¥${d.value.amount.amount}`;
234
- }
109
+ if (d.value.__typename === 'DiscountPercentage') return `${100 - d.value.percentage} 折`;
110
+ if (d.value.__typename === 'DiscountAmount') return `减 ¥${d.value.amount.amount}`;
235
111
  return '免运费';
236
112
  }
237
113
 
238
114
  // ============================================================
239
- // 5. DiscountClaimButton
115
+ // DiscountClaimButton
240
116
  // ============================================================
241
117
 
242
118
  export interface DiscountClaimButtonProps {
@@ -244,6 +120,8 @@ export interface DiscountClaimButtonProps {
244
120
  children?: React.ReactNode;
245
121
  loadingText?: React.ReactNode;
246
122
  claimedText?: React.ReactNode;
123
+ /** 未登录时跳转的路径。默认 `/login?next=` + 当前路径 */
124
+ loginPath?: string;
247
125
  onClaimed?: () => void;
248
126
  onError?: (msg: string) => void;
249
127
  className?: string;
@@ -253,9 +131,10 @@ export function DiscountClaimButton(props: DiscountClaimButtonProps) {
253
131
  const {
254
132
  discount, children = '领取',
255
133
  loadingText = '领取中…', claimedText = '已领取',
134
+ loginPath,
256
135
  onClaimed, onError, className,
257
136
  } = props;
258
- const { claim, myDiscounts } = useDiscounts();
137
+ const { claim, myDiscounts, myDiscountsStatus } = useDiscounts();
259
138
  const analytics = useAnalytics();
260
139
  const [loading, setLoading] = React.useState(false);
261
140
  const [err, setErr] = React.useState<string | null>(null);
@@ -263,6 +142,13 @@ export function DiscountClaimButton(props: DiscountClaimButtonProps) {
263
142
  const alreadyClaimed = myDiscounts.some((c) => c.discount.id === discount.id);
264
143
 
265
144
  const handleClaim = async () => {
145
+ // 未登录 → 跳 login
146
+ if (myDiscountsStatus === 'unauthenticated') {
147
+ if (typeof window === 'undefined') return;
148
+ const next = encodeURIComponent(window.location.pathname + window.location.search);
149
+ window.location.href = loginPath || `/login?next=${next}`;
150
+ return;
151
+ }
266
152
  if (!discount.code) return;
267
153
  setLoading(true);
268
154
  setErr(null);
@@ -303,13 +189,12 @@ export function DiscountClaimButton(props: DiscountClaimButtonProps) {
303
189
  }
304
190
 
305
191
  // ============================================================
306
- // 6. MyDiscountList (我的卡包)
192
+ // MyDiscountList (我的卡包)
307
193
  // ============================================================
308
194
 
309
195
  export interface MyDiscountListProps {
310
196
  className?: string;
311
197
  emptyText?: React.ReactNode;
312
- /** 过滤 tabs(available / used / expired),默认显示 available */
313
198
  filter?: 'available' | 'used' | 'expired' | 'all';
314
199
  }
315
200
 
@@ -324,7 +209,6 @@ export function MyDiscountList(props: MyDiscountListProps) {
324
209
  if (filter === 'all') return true;
325
210
  if (filter === 'expired') return c.isExpired;
326
211
  if (filter === 'used') return c.remainingUses === 0 && !c.isExpired;
327
- // available
328
212
  return !c.isExpired && c.remainingUses > 0;
329
213
  });
330
214