@shopbb/helium 0.3.1 → 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.
- package/dist/components/AddressBookProvider.d.ts +93 -0
- package/dist/components/AddressBookProvider.d.ts.map +1 -0
- package/dist/components/AddressBookProvider.js +182 -0
- package/dist/components/AddressBookProvider.js.map +1 -0
- package/dist/components/AddressForm.d.ts +54 -0
- package/dist/components/AddressForm.d.ts.map +1 -0
- package/dist/components/AddressForm.js +87 -0
- package/dist/components/AddressForm.js.map +1 -0
- package/dist/components/AddressList.d.ts +35 -0
- package/dist/components/AddressList.d.ts.map +1 -0
- package/dist/components/AddressList.js +40 -0
- package/dist/components/AddressList.js.map +1 -0
- package/dist/components/AddressPicker.d.ts +39 -0
- package/dist/components/AddressPicker.d.ts.map +1 -0
- package/dist/components/AddressPicker.js +74 -0
- package/dist/components/AddressPicker.js.map +1 -0
- package/dist/components/CartProvider.d.ts.map +1 -1
- package/dist/components/CartProvider.js +9 -0
- package/dist/components/CartProvider.js.map +1 -1
- package/dist/components/DiscountComponents.d.ts +49 -0
- package/dist/components/DiscountComponents.d.ts.map +1 -0
- package/dist/components/DiscountComponents.js +119 -0
- package/dist/components/DiscountComponents.js.map +1 -0
- package/dist/components/DiscountProvider.d.ts +136 -0
- package/dist/components/DiscountProvider.d.ts.map +1 -0
- package/dist/components/DiscountProvider.js +262 -0
- package/dist/components/DiscountProvider.js.map +1 -0
- package/dist/components/DiscountSelector.d.ts +36 -0
- package/dist/components/DiscountSelector.d.ts.map +1 -0
- package/dist/components/DiscountSelector.js +111 -0
- package/dist/components/DiscountSelector.js.map +1 -0
- package/dist/components/index.d.ts +14 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +9 -0
- package/dist/components/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AddressBookProvider.tsx +279 -0
- package/src/components/AddressForm.tsx +198 -0
- package/src/components/AddressList.tsx +110 -0
- package/src/components/AddressPicker.tsx +152 -0
- package/src/components/CartProvider.tsx +9 -0
- package/src/components/DiscountComponents.tsx +253 -0
- package/src/components/DiscountProvider.tsx +390 -0
- package/src/components/DiscountSelector.tsx +220 -0
- package/src/components/index.ts +61 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <AddressPicker>
|
|
3
|
+
*
|
|
4
|
+
* Checkout 用的地址选择器。**自带**:
|
|
5
|
+
* - 从 useAddressBook() 拿 list
|
|
6
|
+
* - radio 渲染 + 默认选 defaultAddress
|
|
7
|
+
* - 选 "新地址" 时展开新增表单
|
|
8
|
+
*
|
|
9
|
+
* 用法(受控):
|
|
10
|
+
* <AddressPicker
|
|
11
|
+
* value={selectedAddressId}
|
|
12
|
+
* onChange={(id, addr) => setSelectedAddressId(id)}
|
|
13
|
+
* allowNewAddress
|
|
14
|
+
* onUseNewAddress={(addr) => setInlineAddress(addr)}
|
|
15
|
+
* />
|
|
16
|
+
*
|
|
17
|
+
* 用法(非受控):
|
|
18
|
+
* <AddressPicker onSelect={(addr) => ...} />
|
|
19
|
+
* 组件内部 state;选择后回调商家拿值。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
import { useAddressBook, type Address } from './AddressBookProvider';
|
|
24
|
+
import { AddressForm } from './AddressForm';
|
|
25
|
+
|
|
26
|
+
export interface AddressPickerProps {
|
|
27
|
+
/** 受控:当前选中的 address ID */
|
|
28
|
+
value?: string | null;
|
|
29
|
+
/** 受控:选中变化(同时拿到 address 对象) */
|
|
30
|
+
onChange?: (addressId: string | null, address: Address | null) => void;
|
|
31
|
+
/** 非受控:选择时回调(不需要外面 state) */
|
|
32
|
+
onSelect?: (address: Address | null) => void;
|
|
33
|
+
/** 是否允许"使用新地址"选项 */
|
|
34
|
+
allowNewAddress?: boolean;
|
|
35
|
+
/** 选了"新地址"后填表保存的回调(拿到新建好的 Address) */
|
|
36
|
+
onUseNewAddress?: (address: Address) => void;
|
|
37
|
+
/** 空状态:没保存地址时显示什么。默认渲染内嵌 AddressForm */
|
|
38
|
+
emptyFallback?: React.ReactNode;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function AddressPicker(props: AddressPickerProps) {
|
|
43
|
+
const {
|
|
44
|
+
value, onChange, onSelect,
|
|
45
|
+
allowNewAddress = true,
|
|
46
|
+
onUseNewAddress,
|
|
47
|
+
emptyFallback,
|
|
48
|
+
className,
|
|
49
|
+
} = props;
|
|
50
|
+
const { addresses, defaultAddress, status } = useAddressBook();
|
|
51
|
+
|
|
52
|
+
// 非受控 fallback
|
|
53
|
+
const [internalId, setInternalId] = React.useState<string | null>(null);
|
|
54
|
+
const selectedId = value !== undefined ? value : internalId;
|
|
55
|
+
const setSelectedId = React.useCallback((id: string | null, addr: Address | null) => {
|
|
56
|
+
if (value === undefined) setInternalId(id);
|
|
57
|
+
onChange?.(id, addr);
|
|
58
|
+
onSelect?.(addr);
|
|
59
|
+
}, [value, onChange, onSelect]);
|
|
60
|
+
|
|
61
|
+
const [showNewForm, setShowNewForm] = React.useState(false);
|
|
62
|
+
|
|
63
|
+
// 初次:自动选 default address
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (selectedId == null && defaultAddress) {
|
|
66
|
+
setSelectedId(defaultAddress.id, defaultAddress);
|
|
67
|
+
} else if (selectedId == null && addresses.length > 0) {
|
|
68
|
+
setSelectedId(addresses[0].id, addresses[0]);
|
|
69
|
+
}
|
|
70
|
+
}, [defaultAddress, addresses, selectedId, setSelectedId]);
|
|
71
|
+
|
|
72
|
+
if (status === 'loading') {
|
|
73
|
+
return <div data-address-picker data-loading>加载地址中…</div>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 0 个地址:直接展示一个新增表单
|
|
77
|
+
if (addresses.length === 0) {
|
|
78
|
+
if (emptyFallback) return <>{emptyFallback}</>;
|
|
79
|
+
return (
|
|
80
|
+
<div data-address-picker data-empty className={className}>
|
|
81
|
+
<div data-picker-title>填写收货地址</div>
|
|
82
|
+
<AddressForm
|
|
83
|
+
onSave={(addr) => {
|
|
84
|
+
if (addr) {
|
|
85
|
+
setSelectedId(addr.id, addr);
|
|
86
|
+
onUseNewAddress?.(addr);
|
|
87
|
+
}
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div data-address-picker className={className}>
|
|
96
|
+
<div data-picker-title>选择收货地址</div>
|
|
97
|
+
<div data-picker-list>
|
|
98
|
+
{addresses.map((a) => (
|
|
99
|
+
<label key={a.id} data-picker-item data-active={selectedId === a.id ? '' : undefined}>
|
|
100
|
+
<input
|
|
101
|
+
type="radio"
|
|
102
|
+
name="address-picker"
|
|
103
|
+
checked={selectedId === a.id}
|
|
104
|
+
onChange={() => { setSelectedId(a.id, a); setShowNewForm(false); }}
|
|
105
|
+
/>
|
|
106
|
+
<div>
|
|
107
|
+
<div data-name>
|
|
108
|
+
{a.firstName || ''}{a.lastName || ''}
|
|
109
|
+
{a.isDefault && <span data-default-tag>默认</span>}
|
|
110
|
+
</div>
|
|
111
|
+
<div data-line>
|
|
112
|
+
{a.phone} · {a.province}{a.city}{a.district || ''}{a.address1 || ''}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</label>
|
|
116
|
+
))}
|
|
117
|
+
{allowNewAddress && (
|
|
118
|
+
<label data-picker-item data-active={showNewForm ? '' : undefined}>
|
|
119
|
+
<input
|
|
120
|
+
type="radio"
|
|
121
|
+
name="address-picker"
|
|
122
|
+
checked={showNewForm}
|
|
123
|
+
onChange={() => { setShowNewForm(true); setSelectedId(null, null); }}
|
|
124
|
+
/>
|
|
125
|
+
<div>
|
|
126
|
+
<div data-name>+ 使用新地址</div>
|
|
127
|
+
<div data-line>填写一个新地址(默认保存到地址簿)</div>
|
|
128
|
+
</div>
|
|
129
|
+
</label>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
{showNewForm && allowNewAddress && (
|
|
133
|
+
<div data-new-address-form>
|
|
134
|
+
<AddressForm
|
|
135
|
+
onCancel={() => {
|
|
136
|
+
setShowNewForm(false);
|
|
137
|
+
const fallback = defaultAddress || addresses[0];
|
|
138
|
+
if (fallback) setSelectedId(fallback.id, fallback);
|
|
139
|
+
}}
|
|
140
|
+
onSave={(addr) => {
|
|
141
|
+
if (addr) {
|
|
142
|
+
setShowNewForm(false);
|
|
143
|
+
setSelectedId(addr.id, addr);
|
|
144
|
+
onUseNewAddress?.(addr);
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discount UI 组件集
|
|
3
|
+
*
|
|
4
|
+
* <AppliedDiscountList> 当前 cart 已应用的折扣分配(只读)
|
|
5
|
+
* <ClaimableDiscountList> 可领取的优惠券列表(首页 / 商品页)
|
|
6
|
+
* <DiscountClaimButton> 单个领取按钮
|
|
7
|
+
* <MyDiscountList> 我的卡包
|
|
8
|
+
*
|
|
9
|
+
* 注:W5 已删除以下组件(旧设计):
|
|
10
|
+
* - <DiscountCodeInput>: 输入框范式已过时。请用 <DiscountSelector> + <ClaimableDiscountList>。
|
|
11
|
+
* - <BestDiscountHint>: 服务端自动选择最佳券,不需要前端提示。用户在 <DiscountSelector> 看到 / 切换。
|
|
12
|
+
*
|
|
13
|
+
* 全部无样式 + data-* 钩子。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
import { useDiscounts, useProductDiscounts, type Discount, type DiscountClaim } from './DiscountProvider';
|
|
18
|
+
import { useAnalytics } from './AnalyticsProvider';
|
|
19
|
+
import { Money } from './Money';
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// AppliedDiscountList
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
export interface AppliedDiscountListProps {
|
|
26
|
+
className?: string;
|
|
27
|
+
emptyText?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function AppliedDiscountList(props: AppliedDiscountListProps) {
|
|
31
|
+
const { className, emptyText = null } = props;
|
|
32
|
+
const { cartAllocations } = useDiscounts();
|
|
33
|
+
|
|
34
|
+
if (cartAllocations.length === 0) return <>{emptyText}</>;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div data-applied-discount-list className={className}>
|
|
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>}
|
|
43
|
+
</div>
|
|
44
|
+
<span data-discount-amount>
|
|
45
|
+
− <Money data={{ amount: alloc.discountedAmount.amount, currencyCode: alloc.discountedAmount.currencyCode }} />
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// ClaimableDiscountList
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
export interface ClaimableDiscountListProps {
|
|
58
|
+
scope?: 'store' | 'product';
|
|
59
|
+
productHandle?: string;
|
|
60
|
+
first?: number;
|
|
61
|
+
className?: string;
|
|
62
|
+
emptyText?: React.ReactNode;
|
|
63
|
+
renderItem?: (discount: Discount) => React.ReactNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ClaimableDiscountList(props: ClaimableDiscountListProps) {
|
|
67
|
+
const { scope = 'store', productHandle, first = 10, className, emptyText = null, renderItem } = props;
|
|
68
|
+
const { publicDiscounts, publicDiscountsStatus } = useDiscounts();
|
|
69
|
+
const { discounts: productDiscs, loading: productLoading } = useProductDiscounts(scope === 'product' ? productHandle : null);
|
|
70
|
+
|
|
71
|
+
const list = scope === 'product' ? productDiscs : publicDiscounts;
|
|
72
|
+
const loading = scope === 'product' ? productLoading : publicDiscountsStatus === 'loading';
|
|
73
|
+
|
|
74
|
+
if (loading) return null;
|
|
75
|
+
if (list.length === 0) return <>{emptyText}</>;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div data-claimable-discount-list data-scope={scope} className={className}>
|
|
79
|
+
{list.slice(0, first).map((d) => (
|
|
80
|
+
<React.Fragment key={d.id}>
|
|
81
|
+
{renderItem ? renderItem(d) : <DefaultClaimableItem discount={d} />}
|
|
82
|
+
</React.Fragment>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function DefaultClaimableItem({ discount: d }: { discount: Discount }) {
|
|
89
|
+
return (
|
|
90
|
+
<div data-claimable-item>
|
|
91
|
+
<div data-info>
|
|
92
|
+
<div data-title>{d.title}</div>
|
|
93
|
+
<div data-value>{formatDiscountValue(d)}</div>
|
|
94
|
+
{d.minSubtotal && (
|
|
95
|
+
<div data-condition>
|
|
96
|
+
满 <Money data={{ amount: d.minSubtotal.amount, currencyCode: d.minSubtotal.currencyCode }} /> 可用
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
{d.endsAt && (
|
|
100
|
+
<div data-deadline>{new Date(d.endsAt).toLocaleDateString('zh-CN')} 过期</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
{d.code && <DiscountClaimButton discount={d}>领取</DiscountClaimButton>}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatDiscountValue(d: Discount): string {
|
|
109
|
+
if (d.value.__typename === 'DiscountPercentage') return `${100 - d.value.percentage} 折`;
|
|
110
|
+
if (d.value.__typename === 'DiscountAmount') return `减 ¥${d.value.amount.amount}`;
|
|
111
|
+
return '免运费';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// DiscountClaimButton
|
|
116
|
+
// ============================================================
|
|
117
|
+
|
|
118
|
+
export interface DiscountClaimButtonProps {
|
|
119
|
+
discount: Discount;
|
|
120
|
+
children?: React.ReactNode;
|
|
121
|
+
loadingText?: React.ReactNode;
|
|
122
|
+
claimedText?: React.ReactNode;
|
|
123
|
+
/** 未登录时跳转的路径。默认 `/login?next=` + 当前路径 */
|
|
124
|
+
loginPath?: string;
|
|
125
|
+
onClaimed?: () => void;
|
|
126
|
+
onError?: (msg: string) => void;
|
|
127
|
+
className?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function DiscountClaimButton(props: DiscountClaimButtonProps) {
|
|
131
|
+
const {
|
|
132
|
+
discount, children = '领取',
|
|
133
|
+
loadingText = '领取中…', claimedText = '已领取',
|
|
134
|
+
loginPath,
|
|
135
|
+
onClaimed, onError, className,
|
|
136
|
+
} = props;
|
|
137
|
+
const { claim, myDiscounts, myDiscountsStatus } = useDiscounts();
|
|
138
|
+
const analytics = useAnalytics();
|
|
139
|
+
const [loading, setLoading] = React.useState(false);
|
|
140
|
+
const [err, setErr] = React.useState<string | null>(null);
|
|
141
|
+
|
|
142
|
+
const alreadyClaimed = myDiscounts.some((c) => c.discount.id === discount.id);
|
|
143
|
+
|
|
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
|
+
}
|
|
152
|
+
if (!discount.code) return;
|
|
153
|
+
setLoading(true);
|
|
154
|
+
setErr(null);
|
|
155
|
+
try {
|
|
156
|
+
const r = await claim(discount.code);
|
|
157
|
+
if (r.userErrors.length > 0) {
|
|
158
|
+
const msg = r.userErrors[0].message;
|
|
159
|
+
setErr(msg);
|
|
160
|
+
onError?.(msg);
|
|
161
|
+
} else {
|
|
162
|
+
onClaimed?.();
|
|
163
|
+
analytics.emit('discount_claim', { code: discount.code });
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
setLoading(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (alreadyClaimed) {
|
|
171
|
+
return <button type="button" className={className} disabled data-discount-claim data-claimed>{claimedText}</button>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className={className}
|
|
179
|
+
onClick={handleClaim}
|
|
180
|
+
disabled={loading}
|
|
181
|
+
data-discount-claim
|
|
182
|
+
data-loading={loading ? '' : undefined}
|
|
183
|
+
>
|
|
184
|
+
{loading ? loadingText : children}
|
|
185
|
+
</button>
|
|
186
|
+
{err && <div data-error>{err}</div>}
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================
|
|
192
|
+
// MyDiscountList (我的卡包)
|
|
193
|
+
// ============================================================
|
|
194
|
+
|
|
195
|
+
export interface MyDiscountListProps {
|
|
196
|
+
className?: string;
|
|
197
|
+
emptyText?: React.ReactNode;
|
|
198
|
+
filter?: 'available' | 'used' | 'expired' | 'all';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function MyDiscountList(props: MyDiscountListProps) {
|
|
202
|
+
const { className, emptyText = '还没有优惠券', filter = 'available' } = props;
|
|
203
|
+
const { myDiscounts, myDiscountsStatus } = useDiscounts();
|
|
204
|
+
|
|
205
|
+
if (myDiscountsStatus === 'loading') return null;
|
|
206
|
+
if (myDiscountsStatus === 'unauthenticated') return null;
|
|
207
|
+
|
|
208
|
+
const filtered = myDiscounts.filter((c) => {
|
|
209
|
+
if (filter === 'all') return true;
|
|
210
|
+
if (filter === 'expired') return c.isExpired;
|
|
211
|
+
if (filter === 'used') return c.remainingUses === 0 && !c.isExpired;
|
|
212
|
+
return !c.isExpired && c.remainingUses > 0;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (filtered.length === 0) return <div data-my-discount-empty>{emptyText}</div>;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div data-my-discount-list className={className}>
|
|
219
|
+
{filtered.map((c) => (
|
|
220
|
+
<div
|
|
221
|
+
key={c.id}
|
|
222
|
+
data-my-discount-item
|
|
223
|
+
data-expired={c.isExpired ? '' : undefined}
|
|
224
|
+
data-used={c.remainingUses === 0 ? '' : undefined}
|
|
225
|
+
>
|
|
226
|
+
<div data-info>
|
|
227
|
+
<div data-title>{c.discount.title}</div>
|
|
228
|
+
<div data-value>{formatClaimValue(c)}</div>
|
|
229
|
+
{c.discount.minSubtotal && (
|
|
230
|
+
<div data-condition>
|
|
231
|
+
满 <Money data={{ amount: c.discount.minSubtotal.amount, currencyCode: c.discount.minSubtotal.currencyCode }} /> 可用
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
{c.expiresAt && (
|
|
235
|
+
<div data-deadline>{new Date(c.expiresAt).toLocaleDateString('zh-CN')} 过期</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
{c.discount.code && <code data-code>{c.discount.code}</code>}
|
|
239
|
+
</div>
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function formatClaimValue(c: DiscountClaim): string {
|
|
246
|
+
if (c.discount.valueType === 'PERCENTAGE' && c.discount.valuePercentage != null) {
|
|
247
|
+
return `${100 - c.discount.valuePercentage} 折`;
|
|
248
|
+
}
|
|
249
|
+
if (c.discount.valueType === 'FIXED_AMOUNT' && c.discount.valueAmount) {
|
|
250
|
+
return `减 ¥${c.discount.valueAmount.amount}`;
|
|
251
|
+
}
|
|
252
|
+
return '免运费';
|
|
253
|
+
}
|