@shopbb/helium 0.6.3 → 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.
- package/dist/components/AddToCartButton.d.ts +17 -22
- package/dist/components/AddToCartButton.d.ts.map +1 -1
- package/dist/components/AddToCartButton.js +8 -45
- package/dist/components/AddToCartButton.js.map +1 -1
- package/dist/components/AddressForm.d.ts +42 -18
- package/dist/components/AddressForm.d.ts.map +1 -1
- package/dist/components/AddressForm.js +23 -20
- package/dist/components/AddressForm.js.map +1 -1
- package/dist/components/AddressList.d.ts +34 -17
- package/dist/components/AddressList.d.ts.map +1 -1
- package/dist/components/AddressList.js +7 -21
- package/dist/components/AddressList.js.map +1 -1
- package/dist/components/AddressPicker.d.ts +14 -16
- package/dist/components/AddressPicker.d.ts.map +1 -1
- package/dist/components/AddressPicker.js +10 -26
- package/dist/components/AddressPicker.js.map +1 -1
- package/dist/components/AnalyticsProvider.d.ts +5 -2
- package/dist/components/AnalyticsProvider.d.ts.map +1 -1
- package/dist/components/AnalyticsProvider.js +13 -11
- package/dist/components/AnalyticsProvider.js.map +1 -1
- package/dist/components/BuyNowButton.d.ts +7 -24
- package/dist/components/BuyNowButton.d.ts.map +1 -1
- package/dist/components/BuyNowButton.js +9 -43
- package/dist/components/BuyNowButton.js.map +1 -1
- package/dist/components/CartCheckoutButton.d.ts +10 -21
- package/dist/components/CartCheckoutButton.d.ts.map +1 -1
- package/dist/components/CartCheckoutButton.js +6 -11
- package/dist/components/CartCheckoutButton.js.map +1 -1
- package/dist/components/CartCost.d.ts +15 -23
- package/dist/components/CartCost.d.ts.map +1 -1
- package/dist/components/CartCost.js +1 -3
- package/dist/components/CartCost.js.map +1 -1
- package/dist/components/CartForm.d.ts +30 -102
- package/dist/components/CartForm.d.ts.map +1 -1
- package/dist/components/CartForm.js +32 -172
- package/dist/components/CartForm.js.map +1 -1
- package/dist/components/DiscountComponents.d.ts +67 -17
- package/dist/components/DiscountComponents.d.ts.map +1 -1
- package/dist/components/DiscountComponents.js +28 -74
- package/dist/components/DiscountComponents.js.map +1 -1
- package/dist/components/DiscountSelector.d.ts +50 -15
- package/dist/components/DiscountSelector.d.ts.map +1 -1
- package/dist/components/DiscountSelector.js +16 -44
- package/dist/components/DiscountSelector.js.map +1 -1
- package/dist/components/hooks/useOptimisticCart.d.ts +36 -37
- package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -1
- package/dist/components/hooks/useOptimisticCart.js +95 -127
- package/dist/components/hooks/useOptimisticCart.js.map +1 -1
- package/dist/components/index.d.ts +24 -45
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +21 -37
- package/dist/components/index.js.map +1 -1
- package/dist/createCartHandler.d.ts.map +1 -1
- package/dist/createCartHandler.js +8 -1
- package/dist/createCartHandler.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/src/components/AddToCartButton.tsx +34 -92
- package/src/components/AddressForm.tsx +56 -26
- package/src/components/AddressList.tsx +42 -33
- package/src/components/AddressPicker.tsx +19 -29
- package/src/components/AnalyticsProvider.tsx +18 -13
- package/src/components/BuyNowButton.tsx +28 -93
- package/src/components/CartCheckoutButton.tsx +16 -33
- package/src/components/CartCost.tsx +16 -28
- package/src/components/CartForm.tsx +87 -231
- package/src/components/DiscountComponents.tsx +94 -100
- package/src/components/DiscountSelector.tsx +68 -49
- package/src/components/hooks/useOptimisticCart.ts +122 -156
- package/src/components/index.ts +51 -99
- package/src/createCartHandler.ts +10 -1
- package/src/index.ts +0 -2
- /package/src/components/{AddressBookProvider.tsx → AddressBookProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/{CartLineQuantityAdjustButton.tsx → CartLineQuantityAdjustButton.tsx.deleted-0.7} +0 -0
- /package/src/components/{CartProvider.tsx → CartProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/{DiscountProvider.tsx → DiscountProvider.tsx.deleted-0.7} +0 -0
- /package/src/components/hooks/{useMounted.ts → useMounted.ts.deleted-0.7} +0 -0
- /package/src/{handleCartFormAction.ts → handleCartFormAction.ts.deleted-0.7} +0 -0
|
@@ -6,38 +6,66 @@
|
|
|
6
6
|
* <DiscountClaimButton> 单个领取按钮
|
|
7
7
|
* <MyDiscountList> 我的卡包
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
61
|
-
|
|
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 {
|
|
70
|
-
|
|
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
|
|
87
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
147
|
-
loadingText = '
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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-
|
|
195
|
+
data-claimed={done ? '' : undefined}
|
|
196
|
+
disabled={done || pending}
|
|
197
|
+
onClick={handleClick}
|
|
198
|
+
className={className}
|
|
197
199
|
>
|
|
198
|
-
{
|
|
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
|
-
|
|
211
|
-
|
|
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 {
|
|
217
|
-
const
|
|
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.
|
|
265
|
-
const
|
|
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.
|
|
270
|
-
return `减 ¥${c.discount.
|
|
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
|
-
* -
|
|
9
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
{
|
|
93
|
-
<span data-amount>− <Money data={{ amount:
|
|
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"
|