@shopbb/helium 0.6.4 → 0.7.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.
Files changed (77) hide show
  1. package/dist/components/AddToCartButton.d.ts +17 -22
  2. package/dist/components/AddToCartButton.d.ts.map +1 -1
  3. package/dist/components/AddToCartButton.js +8 -45
  4. package/dist/components/AddToCartButton.js.map +1 -1
  5. package/dist/components/AddressForm.d.ts +42 -18
  6. package/dist/components/AddressForm.d.ts.map +1 -1
  7. package/dist/components/AddressForm.js +23 -20
  8. package/dist/components/AddressForm.js.map +1 -1
  9. package/dist/components/AddressList.d.ts +34 -17
  10. package/dist/components/AddressList.d.ts.map +1 -1
  11. package/dist/components/AddressList.js +7 -21
  12. package/dist/components/AddressList.js.map +1 -1
  13. package/dist/components/AddressPicker.d.ts +14 -16
  14. package/dist/components/AddressPicker.d.ts.map +1 -1
  15. package/dist/components/AddressPicker.js +10 -26
  16. package/dist/components/AddressPicker.js.map +1 -1
  17. package/dist/components/AnalyticsProvider.d.ts +5 -2
  18. package/dist/components/AnalyticsProvider.d.ts.map +1 -1
  19. package/dist/components/AnalyticsProvider.js +13 -11
  20. package/dist/components/AnalyticsProvider.js.map +1 -1
  21. package/dist/components/BuyNowButton.d.ts +7 -24
  22. package/dist/components/BuyNowButton.d.ts.map +1 -1
  23. package/dist/components/BuyNowButton.js +9 -43
  24. package/dist/components/BuyNowButton.js.map +1 -1
  25. package/dist/components/CartCheckoutButton.d.ts +10 -21
  26. package/dist/components/CartCheckoutButton.d.ts.map +1 -1
  27. package/dist/components/CartCheckoutButton.js +6 -11
  28. package/dist/components/CartCheckoutButton.js.map +1 -1
  29. package/dist/components/CartCost.d.ts +15 -23
  30. package/dist/components/CartCost.d.ts.map +1 -1
  31. package/dist/components/CartCost.js +1 -3
  32. package/dist/components/CartCost.js.map +1 -1
  33. package/dist/components/CartForm.d.ts +30 -102
  34. package/dist/components/CartForm.d.ts.map +1 -1
  35. package/dist/components/CartForm.js +32 -172
  36. package/dist/components/CartForm.js.map +1 -1
  37. package/dist/components/DiscountComponents.d.ts +67 -17
  38. package/dist/components/DiscountComponents.d.ts.map +1 -1
  39. package/dist/components/DiscountComponents.js +28 -74
  40. package/dist/components/DiscountComponents.js.map +1 -1
  41. package/dist/components/DiscountSelector.d.ts +50 -15
  42. package/dist/components/DiscountSelector.d.ts.map +1 -1
  43. package/dist/components/DiscountSelector.js +16 -44
  44. package/dist/components/DiscountSelector.js.map +1 -1
  45. package/dist/components/hooks/useOptimisticCart.d.ts +36 -37
  46. package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -1
  47. package/dist/components/hooks/useOptimisticCart.js +95 -127
  48. package/dist/components/hooks/useOptimisticCart.js.map +1 -1
  49. package/dist/components/index.d.ts +24 -45
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/index.js +21 -37
  52. package/dist/components/index.js.map +1 -1
  53. package/dist/index.d.ts +0 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +0 -1
  56. package/dist/index.js.map +1 -1
  57. package/package.json +4 -10
  58. package/src/components/AddToCartButton.tsx +34 -92
  59. package/src/components/AddressForm.tsx +56 -26
  60. package/src/components/AddressList.tsx +42 -33
  61. package/src/components/AddressPicker.tsx +19 -29
  62. package/src/components/AnalyticsProvider.tsx +18 -13
  63. package/src/components/BuyNowButton.tsx +28 -93
  64. package/src/components/CartCheckoutButton.tsx +16 -33
  65. package/src/components/CartCost.tsx +16 -28
  66. package/src/components/CartForm.tsx +87 -231
  67. package/src/components/DiscountComponents.tsx +94 -100
  68. package/src/components/DiscountSelector.tsx +68 -49
  69. package/src/components/hooks/useOptimisticCart.ts +122 -156
  70. package/src/components/index.ts +51 -99
  71. package/src/index.ts +0 -2
  72. /package/src/components/{AddressBookProvider.tsx → AddressBookProvider.tsx.deleted-0.7} +0 -0
  73. /package/src/components/{CartLineQuantityAdjustButton.tsx → CartLineQuantityAdjustButton.tsx.deleted-0.7} +0 -0
  74. /package/src/components/{CartProvider.tsx → CartProvider.tsx.deleted-0.7} +0 -0
  75. /package/src/components/{DiscountProvider.tsx → DiscountProvider.tsx.deleted-0.7} +0 -0
  76. /package/src/components/hooks/{useMounted.ts → useMounted.ts.deleted-0.7} +0 -0
  77. /package/src/{handleCartFormAction.ts → handleCartFormAction.ts.deleted-0.7} +0 -0
@@ -6,38 +6,66 @@
6
6
  * <DiscountClaimButton> 单个领取按钮
7
7
  * <MyDiscountList> 我的卡包
8
8
  *
9
- * 注:W5 已删除以下组件(旧设计):
10
- * - <DiscountCodeInput>: 输入框范式已过时。请用 <DiscountSelector> + <ClaimableDiscountList>。
11
- * - <BestDiscountHint>: 服务端自动选择最佳券,不需要前端提示。用户在 <DiscountSelector> 看到 / 切换。
12
- *
13
- * 全部无样式 + data-* 钩子。
9
+ * helium 0.7:所有组件**接 props 不靠 Provider**。
10
+ * 商家从 loader publicDiscounts / myClaims / cart.discountAllocations 传入。
14
11
  */
15
12
 
16
13
  import * as React from 'react';
17
- import { useDiscounts, useProductDiscounts, type Discount, type DiscountClaim } from './DiscountProvider';
18
- import { useAnalytics } from './AnalyticsProvider';
19
14
  import { Money } from './Money';
20
- import { useMounted } from './hooks/useMounted';
21
15
  import { formatDate } from '../utils/formatDate';
22
16
 
17
+ // 类型定义
18
+ export type DiscountValueType = 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FREE_SHIPPING';
19
+
20
+ export interface DiscountValuePercentage { __typename: 'DiscountPercentage'; percentage: number; }
21
+ export interface DiscountValueAmount { __typename: 'DiscountAmount'; amount: { amount: string; currencyCode: string }; }
22
+ export interface DiscountValueFreeShipping { __typename: 'DiscountFreeShipping'; freeShipping: boolean; }
23
+
24
+ export interface Discount {
25
+ id: string;
26
+ code: string | null;
27
+ title: string;
28
+ description: string | null;
29
+ valueType: DiscountValueType;
30
+ value: DiscountValuePercentage | DiscountValueAmount | DiscountValueFreeShipping;
31
+ minSubtotal: { amount: string; currencyCode: string } | null;
32
+ startsAt: string | null;
33
+ endsAt: string | null;
34
+ }
35
+
36
+ export interface DiscountClaim {
37
+ id: string;
38
+ claimedAt: string;
39
+ expiresAt: string | null;
40
+ isExpired: boolean;
41
+ remainingUses: number;
42
+ usedCount: number;
43
+ discount: Discount;
44
+ }
45
+
46
+ export interface DiscountAllocation {
47
+ discountedAmount: { amount: string; currencyCode: string };
48
+ title?: string | null;
49
+ code?: string | null;
50
+ }
51
+
23
52
  // ============================================================
24
53
  // AppliedDiscountList
25
54
  // ============================================================
26
55
 
27
56
  export interface AppliedDiscountListProps {
57
+ /** 从 cart.discountAllocations 传入 */
58
+ allocations?: DiscountAllocation[];
28
59
  className?: string;
29
60
  emptyText?: React.ReactNode;
30
61
  }
31
62
 
32
63
  export function AppliedDiscountList(props: AppliedDiscountListProps) {
33
- const { className, emptyText = null } = props;
34
- const { cartAllocations } = useDiscounts();
35
-
36
- if (cartAllocations.length === 0) return <>{emptyText}</>;
37
-
64
+ const { allocations = [], className, emptyText = null } = props;
65
+ if (allocations.length === 0) return <>{emptyText}</>;
38
66
  return (
39
67
  <div data-applied-discount-list className={className}>
40
- {cartAllocations.map((alloc, i) => (
68
+ {allocations.map((alloc, i) => (
41
69
  <div key={alloc.code || i} data-applied-item>
42
70
  <div data-info>
43
71
  <span data-title>{alloc.title}</span>
@@ -57,8 +85,9 @@ export function AppliedDiscountList(props: AppliedDiscountListProps) {
57
85
  // ============================================================
58
86
 
59
87
  export interface ClaimableDiscountListProps {
60
- scope?: 'store' | 'product';
61
- productHandle?: string;
88
+ /** 可领取的折扣(从 loader 拉,例如 publicDiscounts 或 productDiscounts) */
89
+ discounts?: Discount[];
90
+ /** 一次最多渲染几张,默认 10 */
62
91
  first?: number;
63
92
  className?: string;
64
93
  emptyText?: React.ReactNode;
@@ -66,25 +95,11 @@ export interface ClaimableDiscountListProps {
66
95
  }
67
96
 
68
97
  export function ClaimableDiscountList(props: ClaimableDiscountListProps) {
69
- const { scope = 'store', productHandle, first = 10, className, emptyText = null, renderItem } = props;
70
- const mounted = useMounted();
71
- const { publicDiscounts, publicDiscountsStatus } = useDiscounts();
72
- const { discounts: productDiscs, loading: productLoading } = useProductDiscounts(scope === 'product' ? productHandle : null);
73
-
74
- const list = scope === 'product' ? productDiscs : publicDiscounts;
75
- const loading = scope === 'product' ? productLoading : publicDiscountsStatus === 'loading';
76
-
77
- // SSR 安全:首次渲染返回固定占位(空容器),避免和 client 拉到数据后的渲染 mismatch
78
- if (!mounted) {
79
- return <div data-claimable-discount-list data-scope={scope} data-ssr-placeholder className={className} />;
80
- }
81
-
82
- if (loading) return null;
83
- if (list.length === 0) return <>{emptyText}</>;
84
-
98
+ const { discounts = [], first = 10, className, emptyText = null, renderItem } = props;
99
+ if (discounts.length === 0) return <>{emptyText}</>;
85
100
  return (
86
- <div data-claimable-discount-list data-scope={scope} className={className}>
87
- {list.slice(0, first).map((d) => (
101
+ <div data-claimable-discount-list className={className}>
102
+ {discounts.slice(0, first).map((d) => (
88
103
  <React.Fragment key={d.id}>
89
104
  {renderItem ? renderItem(d) : <DefaultClaimableItem discount={d} />}
90
105
  </React.Fragment>
@@ -115,9 +130,7 @@ function DefaultClaimableItem({ discount: d }: { discount: Discount }) {
115
130
 
116
131
  function formatDiscountValue(d: Discount): string {
117
132
  if (d.value.__typename === 'DiscountPercentage') {
118
- // percentage 表示折扣比例(10 = 减 10%)→ 中文"折"是付款比例(10% off = 付 90% = 9 折)
119
133
  const pay = (100 - d.value.percentage) / 10;
120
- // 整数省点:8 折 / 7.5 折
121
134
  const txt = Number.isInteger(pay) ? String(pay) : pay.toFixed(1);
122
135
  return `${txt} 折`;
123
136
  }
@@ -126,76 +139,65 @@ function formatDiscountValue(d: Discount): string {
126
139
  }
127
140
 
128
141
  // ============================================================
129
- // DiscountClaimButton
142
+ // DiscountClaimButton — 领取优惠券(需要登录)
143
+ //
144
+ // helium 0.7:不再调 DiscountProvider.claim()。商家自己提供 onClaim 回调或
145
+ // 用 <Form action="/account/discounts" method="POST"> 包裹。
146
+ // 这里给一个最简单的"调商家自传 action endpoint"实现。
130
147
  // ============================================================
131
148
 
132
149
  export interface DiscountClaimButtonProps {
133
150
  discount: Discount;
134
- children?: React.ReactNode;
151
+ /** 点击触发的回调(商家实现 claim 逻辑) */
152
+ onClaim?: (discount: Discount) => Promise<void> | void;
153
+ /** 是否已领(外部判断后传入,按钮 disabled) */
154
+ claimed?: boolean;
155
+ /** 加载中文本 */
135
156
  loadingText?: React.ReactNode;
157
+ /** 已领文本 */
136
158
  claimedText?: React.ReactNode;
137
- /** 未登录时跳转的路径。默认 `/login?next=` + 当前路径 */
138
- loginPath?: string;
139
- onClaimed?: () => void;
140
- onError?: (msg: string) => void;
159
+ children?: React.ReactNode;
141
160
  className?: string;
142
161
  }
143
162
 
144
163
  export function DiscountClaimButton(props: DiscountClaimButtonProps) {
145
164
  const {
146
- discount, children = '领取',
147
- loadingText = '领取中…', claimedText = '已领取',
148
- loginPath,
149
- onClaimed, onError, className,
165
+ discount, onClaim, claimed = false,
166
+ loadingText = '领取中...',
167
+ claimedText = '已领取',
168
+ children = '领取',
169
+ className,
150
170
  } = props;
151
- const { claim, myDiscounts, myDiscountsStatus } = useDiscounts();
152
- const analytics = useAnalytics();
153
- const [loading, setLoading] = React.useState(false);
154
- const [err, setErr] = React.useState<string | null>(null);
155
171
 
156
- const alreadyClaimed = myDiscounts.some((c) => c.discount.id === discount.id);
172
+ const [pending, setPending] = React.useState(false);
173
+ const [err, setErr] = React.useState<string | null>(null);
174
+ const [done, setDone] = React.useState(claimed);
157
175
 
158
- const handleClaim = async () => {
159
- // 未登录 login
160
- if (myDiscountsStatus === 'unauthenticated') {
161
- if (typeof window === 'undefined') return;
162
- const next = encodeURIComponent(window.location.pathname + window.location.search);
163
- window.location.href = loginPath || `/login?next=${next}`;
164
- return;
165
- }
166
- if (!discount.code) return;
167
- setLoading(true);
176
+ const handleClick = async () => {
177
+ if (done || pending) return;
178
+ setPending(true);
168
179
  setErr(null);
169
180
  try {
170
- const r = await claim(discount.code);
171
- if (r.userErrors.length > 0) {
172
- const msg = r.userErrors[0].message;
173
- setErr(msg);
174
- onError?.(msg);
175
- } else {
176
- onClaimed?.();
177
- analytics.emit('discount_claim', { code: discount.code });
178
- }
181
+ await onClaim?.(discount);
182
+ setDone(true);
183
+ } catch (e: any) {
184
+ setErr(e?.message || '领取失败');
179
185
  } finally {
180
- setLoading(false);
186
+ setPending(false);
181
187
  }
182
188
  };
183
189
 
184
- if (alreadyClaimed) {
185
- return <button type="button" className={className} disabled data-discount-claim data-claimed>{claimedText}</button>;
186
- }
187
-
188
190
  return (
189
191
  <>
190
192
  <button
191
193
  type="button"
192
- className={className}
193
- onClick={handleClaim}
194
- disabled={loading}
195
194
  data-discount-claim
196
- data-loading={loading ? '' : undefined}
195
+ data-claimed={done ? '' : undefined}
196
+ disabled={done || pending}
197
+ onClick={handleClick}
198
+ className={className}
197
199
  >
198
- {loading ? loadingText : children}
200
+ {pending ? loadingText : done ? claimedText : children}
199
201
  </button>
200
202
  {err && <div data-error>{err}</div>}
201
203
  </>
@@ -203,35 +205,26 @@ export function DiscountClaimButton(props: DiscountClaimButtonProps) {
203
205
  }
204
206
 
205
207
  // ============================================================
206
- // MyDiscountList (我的卡包)
208
+ // MyDiscountList 我的卡包
207
209
  // ============================================================
208
210
 
209
211
  export interface MyDiscountListProps {
210
- className?: string;
211
- emptyText?: React.ReactNode;
212
+ /** 从 customer GraphQL 拉的 claims */
213
+ claims?: DiscountClaim[];
212
214
  filter?: 'available' | 'used' | 'expired' | 'all';
215
+ emptyText?: React.ReactNode;
216
+ className?: string;
213
217
  }
214
218
 
215
219
  export function MyDiscountList(props: MyDiscountListProps) {
216
- const { className, emptyText = '还没有优惠券', filter = 'available' } = props;
217
- const mounted = useMounted();
218
- const { myDiscounts, myDiscountsStatus } = useDiscounts();
219
-
220
- if (!mounted) {
221
- return <div data-my-discount-list data-ssr-placeholder className={className} />;
222
- }
223
- if (myDiscountsStatus === 'loading') return null;
224
- if (myDiscountsStatus === 'unauthenticated') return null;
225
-
226
- const filtered = myDiscounts.filter((c) => {
220
+ const { claims = [], filter = 'available', emptyText = '还没有优惠券', className } = props;
221
+ const filtered = claims.filter((c) => {
227
222
  if (filter === 'all') return true;
228
223
  if (filter === 'expired') return c.isExpired;
229
224
  if (filter === 'used') return c.remainingUses === 0 && !c.isExpired;
230
225
  return !c.isExpired && c.remainingUses > 0;
231
226
  });
232
-
233
227
  if (filtered.length === 0) return <div data-my-discount-empty>{emptyText}</div>;
234
-
235
228
  return (
236
229
  <div data-my-discount-list className={className}>
237
230
  {filtered.map((c) => (
@@ -261,13 +254,14 @@ export function MyDiscountList(props: MyDiscountListProps) {
261
254
  }
262
255
 
263
256
  function formatClaimValue(c: DiscountClaim): string {
264
- if (c.discount.valueType === 'PERCENTAGE' && c.discount.valuePercentage != null) {
265
- const pay = (100 - c.discount.valuePercentage) / 10;
257
+ if (c.discount.value.__typename === 'DiscountPercentage') {
258
+ const pct = c.discount.value.percentage;
259
+ const pay = (100 - pct) / 10;
266
260
  const txt = Number.isInteger(pay) ? String(pay) : pay.toFixed(1);
267
261
  return `${txt} 折`;
268
262
  }
269
- if (c.discount.valueType === 'FIXED_AMOUNT' && c.discount.valueAmount) {
270
- return `减 ¥${c.discount.valueAmount.amount}`;
263
+ if (c.discount.value.__typename === 'DiscountAmount') {
264
+ return `减 ¥${c.discount.value.amount.amount}`;
271
265
  }
272
266
  return '免运费';
273
267
  }
@@ -1,58 +1,83 @@
1
1
  /**
2
2
  * <DiscountSelector>
3
3
  *
4
- * Cart 上的优惠券选择器 — 卡片式 UI,对齐拼多多 / 京东体验。
4
+ * Cart 上的优惠券选择器 — 卡片式 UI
5
5
  *
6
- * 行为:
7
- * - 默认显示当前应用的券(服务端自动选了最佳,或买家显式选的)
8
- * - "切换"打开抽屉/弹窗,列出该买家所有可用券(从 useDiscounts().myDiscounts)
9
- * - 选另一张 cartDiscountSelect mutation
10
- * - 点"不使用优惠券" → cartDiscountClear
11
- *
12
- * 未登录买家:渲染"登录后享受优惠"提示。
13
- *
14
- * 设计目标:
15
- * - 这是 helium "脚手架" 的卡片式实现。商家可以替换为抽屉式 / 弹窗式自定义 UI。
16
- * - 商家也可以直接用 useDiscounts() 自己渲染(提供 hook-only 路径)。
6
+ * helium 0.7 改造:
7
+ * - 数据通过 props 传入(不再用 DiscountProvider)
8
+ * - mutation 通过 <CartForm action="CustomDiscountSelect" inputs={{claimId}}>
9
+ * - 商家 loader myDiscountClaims + cart.appliedDiscountClaim,传给本组件
17
10
  */
18
11
 
19
12
  import * as React from 'react';
20
- import { useDiscounts, type DiscountClaim } from './DiscountProvider';
21
13
  import { Money } from './Money';
22
- import { useMounted } from './hooks/useMounted';
23
14
  import { CartForm } from './CartForm';
24
15
  import { formatDate } from '../utils/formatDate';
25
16
 
17
+ // Re-export 类型用法,对齐之前 DiscountProvider 的 DiscountClaim 类型
18
+ export interface DiscountClaim {
19
+ id: string;
20
+ claimedAt: string;
21
+ expiresAt: string | null;
22
+ isExpired: boolean;
23
+ remainingUses: number;
24
+ discount: {
25
+ id: string;
26
+ code: string | null;
27
+ title: string;
28
+ valueType: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FREE_SHIPPING';
29
+ valuePercentage: number | null;
30
+ valueAmount: { amount: string; currencyCode: string } | null;
31
+ minSubtotal: { amount: string; currencyCode: string } | null;
32
+ };
33
+ }
34
+
35
+ export interface AppliedDiscountClaim {
36
+ claimId: string;
37
+ code: string | null;
38
+ title: string;
39
+ }
40
+
41
+ export interface DiscountAllocation {
42
+ discountedAmount: { amount: string; currencyCode: string };
43
+ title?: string | null;
44
+ code?: string | null;
45
+ }
46
+
26
47
  export interface DiscountSelectorProps {
27
- className?: string;
28
- /** 未登录时显示的内容;不传则显示默认 */
48
+ /** 买家已领的所有 claim(从 customer GraphQL 拉,商家传入) */
49
+ myClaims?: DiscountClaim[];
50
+ /** cart 上当前已应用的 claim(cart.appliedDiscountClaim) */
51
+ appliedClaim?: AppliedDiscountClaim | null;
52
+ /** cart 折扣金额(cart.discountAllocations[0]) */
53
+ appliedAllocation?: DiscountAllocation | null;
54
+ /** 未登录时显示内容 */
55
+ unauthenticated?: boolean;
29
56
  unauthenticatedFallback?: React.ReactNode;
30
- /** 自定义渲染 */
31
- renderApplied?: (props: { code: string | null; title: string; amount: { amount: string; currencyCode: string }; onChange: () => void }) => React.ReactNode;
57
+ className?: string;
58
+ /** 自定义渲染已选 state */
59
+ renderApplied?: (props: {
60
+ code: string | null;
61
+ title: string;
62
+ amount: { amount: string; currencyCode: string };
63
+ onChange: () => void;
64
+ }) => React.ReactNode;
32
65
  }
33
66
 
34
67
  export function DiscountSelector(props: DiscountSelectorProps) {
35
- const { className, unauthenticatedFallback, renderApplied } = props;
36
- const mounted = useMounted();
37
68
  const {
38
- myDiscounts, myDiscountsStatus,
39
- cartAllocations, appliedClaim,
40
- } = useDiscounts();
69
+ myClaims = [],
70
+ appliedClaim,
71
+ appliedAllocation,
72
+ unauthenticated = false,
73
+ unauthenticatedFallback,
74
+ className,
75
+ renderApplied,
76
+ } = props;
41
77
 
42
78
  const [open, setOpen] = React.useState(false);
43
79
 
44
- // SSR + client 首次 render:返回固定占位,保证 hydration 不 mismatch。
45
- if (!mounted) {
46
- return <div data-discount-selector data-ssr-placeholder className={className} />;
47
- }
48
-
49
- // 服务端已经决定了 cart 上是哪张券
50
- const appliedAlloc = cartAllocations[0];
51
- const appliedClaimId = appliedClaim?.claimId ?? null;
52
- const appliedTitle = appliedClaim?.title ?? appliedAlloc?.title;
53
- const appliedCode = appliedClaim?.code ?? appliedAlloc?.code;
54
- // 未登录
55
- if (myDiscountsStatus === 'unauthenticated') {
80
+ if (unauthenticated) {
56
81
  return unauthenticatedFallback !== undefined ? <>{unauthenticatedFallback}</> : (
57
82
  <div data-discount-selector data-unauth className={className}>
58
83
  <span>登录后可使用优惠券</span>
@@ -61,13 +86,13 @@ export function DiscountSelector(props: DiscountSelectorProps) {
61
86
  );
62
87
  }
63
88
 
64
- if (myDiscountsStatus === 'loading') {
65
- return <div data-discount-selector data-loading className={className}>加载优惠券…</div>;
66
- }
89
+ const appliedClaimId = appliedClaim?.claimId ?? null;
90
+ const appliedTitle = appliedClaim?.title ?? appliedAllocation?.title;
91
+ const appliedCode = appliedClaim?.code ?? appliedAllocation?.code;
67
92
 
68
- const availableClaims = myDiscounts.filter((c) => !c.isExpired && c.remainingUses > 0);
93
+ const availableClaims = myClaims.filter((c) => !c.isExpired && c.remainingUses > 0);
69
94
 
70
- if (availableClaims.length === 0) {
95
+ if (availableClaims.length === 0 && !appliedClaimId) {
71
96
  return (
72
97
  <div data-discount-selector data-no-discount className={className}>
73
98
  <span>暂无可用优惠券</span>
@@ -82,15 +107,15 @@ export function DiscountSelector(props: DiscountSelectorProps) {
82
107
  renderApplied ? renderApplied({
83
108
  code: appliedCode ?? null,
84
109
  title: appliedTitle ?? '',
85
- amount: appliedAlloc?.discountedAmount ?? { amount: '0', currencyCode: 'CNY' },
110
+ amount: appliedAllocation?.discountedAmount ?? { amount: '0', currencyCode: 'CNY' },
86
111
  onChange: () => setOpen(true),
87
112
  }) : (
88
113
  <div data-applied>
89
114
  <div data-info>
90
115
  <span data-label>已使用</span>
91
116
  <strong data-title>{appliedTitle}</strong>
92
- {appliedAlloc && (
93
- <span data-amount>− <Money data={{ amount: appliedAlloc.discountedAmount.amount, currencyCode: appliedAlloc.discountedAmount.currencyCode }} /></span>
117
+ {appliedAllocation && (
118
+ <span data-amount>− <Money data={{ amount: appliedAllocation.discountedAmount.amount, currencyCode: appliedAllocation.discountedAmount.currencyCode }} /></span>
94
119
  )}
95
120
  </div>
96
121
  <div data-actions>
@@ -119,10 +144,6 @@ export function DiscountSelector(props: DiscountSelectorProps) {
119
144
  );
120
145
  }
121
146
 
122
- /**
123
- * 抽屉式选券面板。
124
- * Demo 实现为屏底固定面板;商家可以替换为 modal / 抽屉等。
125
- */
126
147
  function DiscountPickerSheet({
127
148
  claims, currentClaimId, onClose,
128
149
  }: {
@@ -139,11 +160,9 @@ function DiscountPickerSheet({
139
160
  </div>
140
161
  <div data-sheet-list>
141
162
  {claims.map((c) => {
142
- // claim_id 既可能是 gid 也可能是 raw —— oxygen mutation 接受两种
143
163
  const id = c.id.replace(/^gid:\/\/shopbb\/DiscountClaim\//, '');
144
164
  const isCurrent = currentClaimId === id;
145
165
  return (
146
- // 每张券一个 <CartForm>,submit 走 /cart action → cart.selectDiscount
147
166
  <CartForm
148
167
  key={c.id}
149
168
  action="CustomDiscountSelect"