@shopbb/helium 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
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 { useCart } from './CartProvider';
22
+ import { useShop } from './ShopProvider';
23
+ import { useAnalytics } from './AnalyticsProvider';
24
+ import { Money } from './Money';
25
+ export function DiscountSelector(props) {
26
+ const { className, unauthenticatedFallback, renderApplied } = props;
27
+ const { myDiscounts, myDiscountsStatus, cartAllocations } = useDiscounts();
28
+ const { cart, refetch: refetchCart } = useCart();
29
+ const shop = useShop();
30
+ const analytics = useAnalytics();
31
+ const [open, setOpen] = React.useState(false);
32
+ const [pending, setPending] = React.useState(false);
33
+ // 服务端已经决定了 cart 上是哪张券
34
+ const appliedAlloc = cartAllocations[0];
35
+ const appliedClaimId = cart?.appliedDiscountClaim?.claimId ?? null;
36
+ const appliedTitle = cart?.appliedDiscountClaim?.title ?? appliedAlloc?.title;
37
+ const appliedCode = cart?.appliedDiscountClaim?.code ?? appliedAlloc?.code;
38
+ const callSelectMutation = async (claimId) => {
39
+ if (!cart)
40
+ return;
41
+ setPending(true);
42
+ try {
43
+ const token = (typeof localStorage !== 'undefined') ? (localStorage.getItem('shopbb:buyer_token') || localStorage.getItem('shopflare:buyer_token')) : null;
44
+ const mutation = claimId
45
+ ? `mutation S($id: ID!, $c: ID!) { cartDiscountSelect(cartId: $id, claimId: $c) { cart { id } userErrors { message } } }`
46
+ : `mutation C($id: ID!) { cartDiscountClear(cartId: $id) { cart { id } userErrors { message } } }`;
47
+ const variables = { id: cart.id };
48
+ if (claimId)
49
+ variables.c = claimId;
50
+ const res = await fetch(shop.apiUrl, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'X-Storefront-Access-Token': shop.storefrontAccessToken,
55
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
56
+ },
57
+ body: JSON.stringify({ query: mutation, variables }),
58
+ credentials: 'include',
59
+ });
60
+ await res.json();
61
+ // 重新拉 cart 让 UI 更新(拿到新的 discountAllocations)
62
+ await refetchCart();
63
+ }
64
+ finally {
65
+ setPending(false);
66
+ setOpen(false);
67
+ }
68
+ };
69
+ // 未登录
70
+ if (myDiscountsStatus === 'unauthenticated') {
71
+ 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" })] }));
72
+ }
73
+ if (myDiscountsStatus === 'loading') {
74
+ return _jsx("div", { "data-discount-selector": true, "data-loading": true, className: className, children: "\u52A0\u8F7D\u4F18\u60E0\u5238\u2026" });
75
+ }
76
+ const availableClaims = myDiscounts.filter((c) => !c.isExpired && c.remainingUses > 0);
77
+ if (availableClaims.length === 0) {
78
+ return (_jsx("div", { "data-discount-selector": true, "data-no-discount": true, className: className, children: _jsx("span", { children: "\u6682\u65E0\u53EF\u7528\u4F18\u60E0\u5238" }) }));
79
+ }
80
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-discount-selector": true, className: className, children: appliedClaimId ? (renderApplied ? renderApplied({
81
+ code: appliedCode ?? null,
82
+ title: appliedTitle ?? '',
83
+ amount: appliedAlloc?.discountedAmount ?? { amount: '0', currencyCode: 'CNY' },
84
+ onChange: () => setOpen(true),
85
+ onClear: () => callSelectMutation(null),
86
+ }) : (_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: () => callSelectMutation(null), 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: (claimId) => {
87
+ analytics.emit('discount_select', { claimId });
88
+ callSelectMutation(claimId);
89
+ }, onClose: () => setOpen(false), pending: pending }))] }));
90
+ }
91
+ /**
92
+ * 抽屉式选券面板。
93
+ * Demo 实现为屏底固定面板;商家可以替换为 modal / 抽屉等。
94
+ */
95
+ function DiscountPickerSheet({ claims, currentClaimId, onSelect, onClose, pending, }) {
96
+ 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) => {
97
+ const id = c.id.replace(/^gid:\/\/shopbb\/DiscountClaim\//, '');
98
+ const isCurrent = currentClaimId === id;
99
+ 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));
100
+ }) })] }) }));
101
+ }
102
+ function formatValue(c) {
103
+ if (c.discount.valueType === 'PERCENTAGE' && c.discount.valuePercentage != null) {
104
+ return `${100 - c.discount.valuePercentage} 折`;
105
+ }
106
+ if (c.discount.valueType === 'FIXED_AMOUNT' && c.discount.valueAmount) {
107
+ return `− ¥${c.discount.valueAmount.amount}`;
108
+ }
109
+ return '免运费';
110
+ }
111
+ //# 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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE1E,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,YAAY,EACV,IAAI,EACJ,QAAQ,EACR,mBAAmB,EACnB,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACjF,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAGzG,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,YAAY,EAAE,iCAAiC,EAAE,MAAM,gCAAgC,CAAC;AAExF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,aAAa,GACd,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,OAAO,EACP,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,EACzB,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,wBAAwB,EACxB,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -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.0",
4
4
  "description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
 
@@ -87,19 +87,19 @@ export interface DiscountContextValue {
87
87
  /** 我领过的有效券 */
88
88
  myDiscounts: DiscountClaim[];
89
89
  myDiscountsStatus: 'unauthenticated' | 'idle' | 'loading' | 'error';
90
- /** 当前 cart 上应用的码 */
91
- appliedToCart: CartDiscountCode[];
92
- /** 当前 cart 上的折扣分配明细 */
90
+ /**
91
+ * 当前 cart 上的折扣分配明细。
92
+ * W5 起:cart 上最多一张券(自动选最佳 / 买家手动选)。
93
+ * 数组语义保留兼容(未来可能允许多张叠加)。
94
+ */
93
95
  cartAllocations: CartDiscountAllocation[];
94
- /** 当前 cart 上最大可省(计算了用户已领但未应用的券) */
95
- bestApplicableForCart: { discount: DiscountClaim; estimatedAmount: number } | null;
96
+ /** 当前应用的 claim ID(W5),null = 无折扣或买家未登录 */
97
+ appliedClaimId: string | null;
96
98
  /** 错误 */
97
99
  error: string | null;
98
100
 
99
101
  // 操作
100
102
  claim: (code: string) => Promise<{ claim?: DiscountClaim; userErrors: DiscountUserError[] }>;
101
- applyToCart: (code: string) => Promise<{ userErrors: DiscountUserError[] }>;
102
- removeFromCart: (code: string) => Promise<{ userErrors: DiscountUserError[] }>;
103
103
  refetchMyDiscounts: () => Promise<void>;
104
104
  refetchPublicDiscounts: () => Promise<void>;
105
105
  }
@@ -280,95 +280,30 @@ export function DiscountProvider(props: DiscountProviderProps) {
280
280
  [customerGql, refetchMyDiscounts],
281
281
  );
282
282
 
283
- // ----- cart apply / remove -----
284
- const appliedToCart = cartCtx?.cart && (cartCtx.cart as any).discountCodes ? (cartCtx.cart as any).discountCodes as CartDiscountCode[] : [];
285
- const cartAllocations = cartCtx?.cart && (cartCtx.cart as any).discountAllocations ? (cartCtx.cart as any).discountAllocations as CartDiscountAllocation[] : [];
286
-
287
- const applyCodes = React.useCallback(
288
- async (codes: string[]) => {
289
- if (!cartCtx?.cart) return { userErrors: [{ code: 'NO_CART', message: 'cart 不存在' }] };
290
- const result = await storefrontGql<{ cartDiscountCodesUpdate: { cart: any; userErrors: DiscountUserError[] } }>(
291
- `mutation U($id: ID!, $codes: [String!]!) {
292
- cartDiscountCodesUpdate(cartId: $id, discountCodes: $codes) {
293
- cart { id }
294
- userErrors { field code message }
295
- }
296
- }`,
297
- { id: cartCtx.cart.id, codes },
298
- );
299
- if (result.errors?.length) {
300
- return { userErrors: [{ code: 'NETWORK_ERROR', message: result.errors[0].message }] };
301
- }
302
- // 刷 cart(拉新的 discountCodes / allocations)
303
- await cartCtx.refetch();
304
- return { userErrors: result.data!.cartDiscountCodesUpdate.userErrors };
305
- },
306
- [cartCtx, storefrontGql],
307
- );
308
-
309
- const applyToCart = React.useCallback(
310
- async (code: string) => {
311
- const existing = appliedToCart.map((d) => d.code);
312
- if (existing.includes(code.toUpperCase())) return { userErrors: [] };
313
- return applyCodes([...existing, code.toUpperCase()]);
314
- },
315
- [appliedToCart, applyCodes],
316
- );
317
-
318
- const removeFromCart = React.useCallback(
319
- async (code: string) => {
320
- const next = appliedToCart.map((d) => d.code).filter((c) => c !== code.toUpperCase());
321
- return applyCodes(next);
322
- },
323
- [appliedToCart, applyCodes],
324
- );
325
-
326
- // ----- best applicable for cart -----
327
- const bestApplicableForCart = React.useMemo(() => {
328
- if (!cartCtx?.cart || myDiscounts.length === 0) return null;
329
- const subtotalStr = (cartCtx.cart as any).cost?.subtotalAmount?.amount;
330
- const subtotal = Number(subtotalStr || 0);
331
- if (subtotal <= 0) return null;
332
-
333
- const currentApplied = new Set(appliedToCart.map((d) => d.code));
334
-
335
- let best: { discount: DiscountClaim; estimatedAmount: number } | null = null;
336
- for (const claimed of myDiscounts) {
337
- const d = claimed.discount;
338
- if (!d.code || claimed.isExpired) continue;
339
- if (currentApplied.has(d.code)) continue;
340
- // 门槛
341
- if (d.minSubtotal && Number(d.minSubtotal.amount) > subtotal) continue;
342
- // 估算折扣
343
- let amount = 0;
344
- if (d.valueType === 'PERCENTAGE' && d.valuePercentage != null) {
345
- amount = Math.floor(subtotal * (d.valuePercentage / 100) * 100) / 100;
346
- } else if (d.valueType === 'FIXED_AMOUNT' && d.valueAmount) {
347
- amount = Math.min(Number(d.valueAmount.amount), subtotal);
348
- }
349
- if (amount > 0 && (best == null || amount > best.estimatedAmount)) {
350
- best = { discount: claimed, estimatedAmount: amount };
351
- }
352
- }
353
- return best;
354
- }, [cartCtx, myDiscounts, appliedToCart]);
283
+ // ----- 当前 cart 上的折扣(服务端算好的) -----
284
+ const cartAllocations = cartCtx?.cart && (cartCtx.cart as any).discountAllocations
285
+ ? (cartCtx.cart as any).discountAllocations as CartDiscountAllocation[]
286
+ : [];
287
+ // W5: 服务端的 appliedDiscountClaim(哪张被选中应用了)
288
+ const appliedClaimId: string | null = cartCtx?.cart && (cartCtx.cart as any).appliedDiscountClaim?.claimId
289
+ ? (cartCtx.cart as any).appliedDiscountClaim.claimId
290
+ : null;
355
291
 
356
292
  const value: DiscountContextValue = React.useMemo(
357
293
  () => ({
358
294
  publicDiscounts, publicDiscountsStatus,
359
295
  myDiscounts, myDiscountsStatus,
360
- appliedToCart, cartAllocations,
361
- bestApplicableForCart,
296
+ cartAllocations,
297
+ appliedClaimId,
362
298
  error,
363
- claim, applyToCart, removeFromCart,
299
+ claim,
364
300
  refetchMyDiscounts, refetchPublicDiscounts,
365
301
  }),
366
302
  [
367
303
  publicDiscounts, publicDiscountsStatus,
368
304
  myDiscounts, myDiscountsStatus,
369
- appliedToCart, cartAllocations,
370
- bestApplicableForCart, error,
371
- claim, applyToCart, removeFromCart,
305
+ cartAllocations, appliedClaimId, error,
306
+ claim,
372
307
  refetchMyDiscounts, refetchPublicDiscounts,
373
308
  ],
374
309
  );