@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.
- 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/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/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
|
@@ -1,97 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <CartForm> —
|
|
2
|
+
* <CartForm> — 严格对齐 Shopify Hydrogen 的 CartForm 实现
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 源文件参考:packages/hydrogen/src/cart/CartForm.tsx
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 核心机制:
|
|
7
|
+
* - 用 react-router 的 useFetcher() + <fetcher.Form>
|
|
8
|
+
* - submit 后 RR7 自动调对应 route 的 action(),完成后自动 revalidate 所有 loader
|
|
9
|
+
* - children 是 render prop 可以接 fetcher 对象(state / data / formData)
|
|
10
|
+
* - fetcherKey 让相同的 mutation 互相 cancel(例如多次点 +/- 只跑最后一次)
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* <CartForm
|
|
12
|
-
* action={CartForm.ACTIONS.LinesAdd}
|
|
13
|
-
* inputs={{ lines: [{ merchandiseId, quantity: 1 }] }}
|
|
14
|
-
* route="/cart"
|
|
15
|
-
* >
|
|
16
|
-
* <button type="submit">加入购物车</button>
|
|
17
|
-
* </CartForm>
|
|
18
|
-
*
|
|
19
|
-
* // 自定义按钮渲染
|
|
20
|
-
* <CartForm action={CartForm.ACTIONS.LinesAdd} inputs={{ lines }}>
|
|
21
|
-
* {(fetcher) => (
|
|
22
|
-
* <button disabled={fetcher.state !== 'idle'}>
|
|
23
|
-
* {fetcher.state === 'submitting' ? '添加中…' : '加入购物车'}
|
|
24
|
-
* </button>
|
|
25
|
-
* )}
|
|
26
|
-
* </CartForm>
|
|
27
|
-
*
|
|
28
|
-
* 服务端(route action):
|
|
29
|
-
*
|
|
30
|
-
* import { CartForm } from '@shopbb/helium';
|
|
31
|
-
*
|
|
32
|
-
* export async function action(request, context) {
|
|
33
|
-
* const formData = await request.formData();
|
|
34
|
-
* const { action, inputs } = CartForm.getFormInput(formData);
|
|
35
|
-
* // ... switch(action) { ... }
|
|
36
|
-
* }
|
|
37
|
-
*
|
|
38
|
-
* ─────────────────────────────────────────────────────────────────
|
|
39
|
-
*
|
|
40
|
-
* 实现说明:
|
|
41
|
-
*
|
|
42
|
-
* - 渲染原生 <form method="POST">,无 JS 也能工作(form 默认提交 = 整页 POST)
|
|
43
|
-
* - INPUT_NAME = "cartFormInput"(hidden input,value = JSON.stringify({action, inputs}))
|
|
44
|
-
* - JS 加载后,由 helium 提供的 useCartFormFetcher hook(或 useFetcher 实现) 拦截 submit 实现"不跳页"
|
|
45
|
-
* - 当前简化实现:hydrate 后 onSubmit preventDefault,自己用 fetch 调,调完后通过 CartProvider.applyCart 更新
|
|
12
|
+
* 与 helium 0.6.x 之前的差异:
|
|
13
|
+
* - 删除自己造的 FetcherContext + raw fetch 实现
|
|
14
|
+
* - peerDep 加 react-router(这就是 Hydrogen 自己的依赖)
|
|
46
15
|
*/
|
|
47
16
|
|
|
48
17
|
import * as React from 'react';
|
|
49
|
-
import {
|
|
50
|
-
import { __registerPendingMutation } from './hooks/useOptimisticCart';
|
|
18
|
+
import { useFetcher, type FetcherWithComponents } from 'react-router';
|
|
51
19
|
|
|
52
20
|
// ============================================================
|
|
53
21
|
// Action 枚举 — 对齐 Hydrogen CartForm.ACTIONS
|
|
54
22
|
// ============================================================
|
|
55
23
|
|
|
56
24
|
export const CART_FORM_ACTIONS = {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/** Remove cart lines */
|
|
76
|
-
LinesRemove: 'LinesRemove' as const,
|
|
77
|
-
/** Update cart note */
|
|
78
|
-
NoteUpdate: 'NoteUpdate' as const,
|
|
79
|
-
/** Update selected delivery options */
|
|
80
|
-
SelectedDeliveryOptionsUpdate: 'SelectedDeliveryOptionsUpdate' as const,
|
|
81
|
-
/** Set metafields */
|
|
82
|
-
MetafieldsSet: 'MetafieldsSet' as const,
|
|
83
|
-
/** Delete metafields */
|
|
84
|
-
MetafieldsDelete: 'MetafieldsDelete' as const,
|
|
25
|
+
AttributesUpdateInput: 'AttributesUpdateInput',
|
|
26
|
+
BuyerIdentityUpdate: 'BuyerIdentityUpdate',
|
|
27
|
+
Create: 'Create',
|
|
28
|
+
DiscountCodesUpdate: 'DiscountCodesUpdate',
|
|
29
|
+
GiftCardCodesUpdate: 'GiftCardCodesUpdate',
|
|
30
|
+
GiftCardCodesAdd: 'GiftCardCodesAdd',
|
|
31
|
+
GiftCardCodesRemove: 'GiftCardCodesRemove',
|
|
32
|
+
LinesAdd: 'LinesAdd',
|
|
33
|
+
LinesRemove: 'LinesRemove',
|
|
34
|
+
LinesUpdate: 'LinesUpdate',
|
|
35
|
+
NoteUpdate: 'NoteUpdate',
|
|
36
|
+
SelectedDeliveryOptionsUpdate: 'SelectedDeliveryOptionsUpdate',
|
|
37
|
+
MetafieldsSet: 'MetafieldsSet',
|
|
38
|
+
MetafieldDelete: 'MetafieldDelete',
|
|
39
|
+
DeliveryAddressesAdd: 'DeliveryAddressesAdd',
|
|
40
|
+
DeliveryAddressesUpdate: 'DeliveryAddressesUpdate',
|
|
41
|
+
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
|
|
42
|
+
DeliveryAddressesReplace: 'DeliveryAddressesReplace',
|
|
85
43
|
} as const;
|
|
86
44
|
|
|
87
45
|
export type CartFormAction = (typeof CART_FORM_ACTIONS)[keyof typeof CART_FORM_ACTIONS] | `Custom${string}`;
|
|
88
46
|
|
|
89
47
|
// ============================================================
|
|
90
|
-
//
|
|
48
|
+
// Input types — 与 Hydrogen 一致(简化版,省略 metafield 等)
|
|
91
49
|
// ============================================================
|
|
92
50
|
|
|
93
|
-
export
|
|
94
|
-
|
|
51
|
+
export interface CartLineInput {
|
|
52
|
+
merchandiseId: string;
|
|
53
|
+
quantity?: number;
|
|
54
|
+
attributes?: Array<{ key: string; value: string }>;
|
|
55
|
+
/** 用于 optimistic UI 展示(不会发到服务端) */
|
|
56
|
+
selectedVariant?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CartLineUpdateInput {
|
|
60
|
+
id: string;
|
|
61
|
+
quantity?: number;
|
|
62
|
+
merchandiseId?: string;
|
|
63
|
+
attributes?: Array<{ key: string; value: string }>;
|
|
64
|
+
}
|
|
95
65
|
|
|
96
66
|
interface OtherFormData {
|
|
97
67
|
[key: string]: unknown;
|
|
@@ -109,203 +79,89 @@ export type CartFormInput =
|
|
|
109
79
|
| { action: `Custom${string}`; inputs: Record<string, unknown> };
|
|
110
80
|
|
|
111
81
|
// ============================================================
|
|
112
|
-
//
|
|
82
|
+
// CartForm
|
|
113
83
|
// ============================================================
|
|
114
84
|
|
|
115
|
-
/** 服务端 / 客户端共享的 hidden field name — 对齐 Hydrogen */
|
|
116
85
|
const INPUT_NAME = 'cartFormInput';
|
|
117
86
|
|
|
118
|
-
/**
|
|
119
|
-
* Fetcher 状态对象 — 模仿 Remix useFetcher
|
|
120
|
-
*
|
|
121
|
-
* 关键属性:
|
|
122
|
-
* - state: 'idle' | 'submitting' | 'loading'
|
|
123
|
-
* - data: 服务端 action 返回的 JSON(成功时)
|
|
124
|
-
* - error: 错误信息
|
|
125
|
-
*/
|
|
126
|
-
export interface CartFormFetcher {
|
|
127
|
-
state: 'idle' | 'submitting' | 'loading';
|
|
128
|
-
data: any;
|
|
129
|
-
error: string | null;
|
|
130
|
-
/** 取最近一次提交的 form data(optimistic UI 用) */
|
|
131
|
-
formData: FormData | null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* CartFormFetcher Context — 让 CartForm 内部任何位置都能用 useFetcher() 拿状态
|
|
136
|
-
*
|
|
137
|
-
* Hydrogen 是用 Remix global fetcher,我们用一个本地 Context。children 既可以是
|
|
138
|
-
* render prop(推荐),也可以是普通节点 + useFetcher() 调用。
|
|
139
|
-
*/
|
|
140
|
-
const FetcherContext = React.createContext<CartFormFetcher | null>(null);
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* 在 <CartForm> 内部任何深度的组件里拿 fetcher 状态。
|
|
144
|
-
*
|
|
145
|
-
* <CartForm action="LinesAdd" inputs={{lines}}>
|
|
146
|
-
* <MyButton />
|
|
147
|
-
* </CartForm>
|
|
148
|
-
*
|
|
149
|
-
* function MyButton() {
|
|
150
|
-
* const fetcher = useFetcher();
|
|
151
|
-
* return <button disabled={fetcher.state !== 'idle'}>添加</button>;
|
|
152
|
-
* }
|
|
153
|
-
*/
|
|
154
|
-
export function useFetcher(): CartFormFetcher {
|
|
155
|
-
const ctx = React.useContext(FetcherContext);
|
|
156
|
-
if (!ctx) {
|
|
157
|
-
throw new Error('useFetcher must be used inside a <CartForm>');
|
|
158
|
-
}
|
|
159
|
-
return ctx;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
87
|
export interface CartFormCommonProps {
|
|
163
|
-
/** children
|
|
164
|
-
children: React.ReactNode | ((fetcher:
|
|
165
|
-
/** 提交的 route
|
|
88
|
+
/** children 可以是普通 React 节点,也可以是 render prop 接收 fetcher 对象 */
|
|
89
|
+
children: React.ReactNode | ((fetcher: FetcherWithComponents<any>) => React.ReactNode);
|
|
90
|
+
/** 提交的 route。默认空字符串表示当前 route */
|
|
166
91
|
route?: string;
|
|
167
|
-
/**
|
|
92
|
+
/** Fetcher key — 相同 key 的 fetcher 会互相 cancel(用于幂等操作)*/
|
|
168
93
|
fetcherKey?: string;
|
|
169
|
-
/** 表单 className(不影响行为) */
|
|
170
|
-
className?: string;
|
|
171
94
|
}
|
|
172
95
|
|
|
173
|
-
// Discriminated union — action + inputs 强制对齐
|
|
174
96
|
export type CartFormProps = CartFormInput & CartFormCommonProps;
|
|
175
97
|
|
|
176
98
|
interface CartFormImpl {
|
|
177
99
|
(props: CartFormProps): React.ReactElement;
|
|
178
100
|
ACTIONS: typeof CART_FORM_ACTIONS;
|
|
179
101
|
INPUT_NAME: typeof INPUT_NAME;
|
|
180
|
-
/**
|
|
181
|
-
* 服务端 helper:从 request.formData() 解析出 { action, inputs }
|
|
182
|
-
*
|
|
183
|
-
* const formData = await request.formData();
|
|
184
|
-
* const { action, inputs } = CartForm.getFormInput(formData);
|
|
185
|
-
*/
|
|
186
102
|
getFormInput: (formData: FormData) => CartFormInput;
|
|
187
103
|
}
|
|
188
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 严格照抄 Hydrogen 实现(packages/hydrogen/src/cart/CartForm.tsx)
|
|
107
|
+
*/
|
|
189
108
|
const CartFormBase = (props: CartFormProps): React.ReactElement => {
|
|
190
|
-
const { children,
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
// 序列化 { action, inputs } 一并放进 hidden input
|
|
194
|
-
// 服务端用 CartForm.getFormInput(formData) 反序列化
|
|
195
|
-
const payload = React.useMemo(() => JSON.stringify({ action, inputs }), [action, inputs]);
|
|
196
|
-
|
|
197
|
-
const [fetcher, setFetcher] = React.useState<CartFormFetcher>({
|
|
198
|
-
state: 'idle',
|
|
199
|
-
data: null,
|
|
200
|
-
error: null,
|
|
201
|
-
formData: null,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// hydrate 后拦截 submit;SSR 时 onSubmit 不存在 → form 原生 POST
|
|
205
|
-
const handleSubmit = React.useCallback(
|
|
206
|
-
async (e: React.FormEvent<HTMLFormElement>) => {
|
|
207
|
-
e.preventDefault();
|
|
208
|
-
const form = e.currentTarget;
|
|
209
|
-
const formData = new FormData(form);
|
|
210
|
-
setFetcher({ state: 'submitting', data: null, error: null, formData });
|
|
211
|
-
|
|
212
|
-
// 注册到 pending store — useOptimisticCart 会看到这个 mutation 立刻应用
|
|
213
|
-
// 只对 cart line 类型的 action 做(DiscountCodesUpdate / Custom* 等不做 optimistic)
|
|
214
|
-
let unregisterPending: (() => void) | null = null;
|
|
215
|
-
if (action === 'LinesAdd' || action === 'LinesUpdate' || action === 'LinesRemove') {
|
|
216
|
-
const pendingId = `${action}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
217
|
-
unregisterPending = __registerPendingMutation({ id: pendingId, action: action as any, inputs });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
// 显式标记 X-Helium-Fetch — 让服务端知道这是 JS fetch 不是原生 form POST
|
|
222
|
-
// 服务端 handleCartFormAction 看到此 header 会返回 JSON 而不是 303 redirect
|
|
223
|
-
const res = await fetch(route, {
|
|
224
|
-
method: 'POST',
|
|
225
|
-
body: formData,
|
|
226
|
-
credentials: 'include',
|
|
227
|
-
headers: { 'Accept': 'application/json', 'X-Helium-Fetch': '1' },
|
|
228
|
-
});
|
|
229
|
-
const data: any = await res.json().catch(() => null);
|
|
230
|
-
if (!res.ok || data?.error || data?.userErrors?.length) {
|
|
231
|
-
throw new Error(data?.error || data?.userErrors?.[0]?.message || `HTTP ${res.status}`);
|
|
232
|
-
}
|
|
233
|
-
// 服务端约定:成功时返回 { cart: Cart }
|
|
234
|
-
// 这里直接 applyCart 到 CartProvider,避免 refetch
|
|
235
|
-
if (data?.cart && cartCtx?.applyCart) {
|
|
236
|
-
cartCtx.applyCart(data.cart);
|
|
237
|
-
}
|
|
238
|
-
setFetcher({ state: 'idle', data, error: null, formData: null });
|
|
239
|
-
} catch (err: any) {
|
|
240
|
-
setFetcher({ state: 'idle', data: null, error: err.message || String(err), formData: null });
|
|
241
|
-
} finally {
|
|
242
|
-
unregisterPending?.();
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
[route, cartCtx, action, inputs],
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const renderedChildren = typeof children === 'function' ? (children as (f: CartFormFetcher) => React.ReactNode)(fetcher) : children;
|
|
109
|
+
const { children, action, inputs, route, fetcherKey } = props as any;
|
|
110
|
+
const fetcher = useFetcher({ key: fetcherKey });
|
|
249
111
|
|
|
250
112
|
return (
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
{renderedChildren}
|
|
262
|
-
</form>
|
|
263
|
-
</FetcherContext.Provider>
|
|
113
|
+
<fetcher.Form action={route || ''} method="post">
|
|
114
|
+
{(action || inputs) && (
|
|
115
|
+
<input
|
|
116
|
+
type="hidden"
|
|
117
|
+
name={INPUT_NAME}
|
|
118
|
+
value={JSON.stringify({ action, inputs })}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{typeof children === 'function' ? children(fetcher) : children}
|
|
122
|
+
</fetcher.Form>
|
|
264
123
|
);
|
|
265
124
|
};
|
|
266
125
|
|
|
267
126
|
/**
|
|
268
127
|
* 服务端 helper:从 request.formData() 还原出 { action, inputs }
|
|
269
128
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* const formData = await request.formData();
|
|
273
|
-
* const { action, inputs } = CartForm.getFormInput(formData);
|
|
129
|
+
* 严格照抄 Hydrogen 实现:把 form 内其它 input(如 <input name="note">)合并进 inputs。
|
|
274
130
|
*/
|
|
275
131
|
function getFormInput(formData: FormData): CartFormInput {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
if (!parsed || typeof parsed.action !== 'string') {
|
|
287
|
-
throw new Error('CartForm.getFormInput: missing "action" in parsed payload');
|
|
132
|
+
// 拿全部 form data
|
|
133
|
+
const data: Record<string, unknown> = {};
|
|
134
|
+
for (const pair of formData.entries()) {
|
|
135
|
+
const key = pair[0];
|
|
136
|
+
const values = formData.getAll(key);
|
|
137
|
+
data[key] = values.length > 1 ? values : pair[1];
|
|
138
|
+
// checkbox → boolean
|
|
139
|
+
if (data[key] === 'on') data[key] = true;
|
|
140
|
+
else if (data[key] === 'off') data[key] = false;
|
|
288
141
|
}
|
|
289
142
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// FormData 里同时有 cartFormInput 和 note 两个字段,inputs.note 来自 FormData 而不是 JSON。
|
|
293
|
-
const otherInputs: Record<string, unknown> = {};
|
|
294
|
-
for (const [key, value] of formData.entries()) {
|
|
295
|
-
if (key === INPUT_NAME) continue;
|
|
296
|
-
otherInputs[key] = value;
|
|
297
|
-
}
|
|
143
|
+
const { cartFormInput, ...otherData } = data;
|
|
144
|
+
const parsed: any = cartFormInput ? JSON.parse(String(cartFormInput)) : {};
|
|
298
145
|
|
|
299
146
|
return {
|
|
300
147
|
action: parsed.action,
|
|
301
|
-
inputs: {
|
|
148
|
+
inputs: {
|
|
149
|
+
...(parsed.inputs || {}),
|
|
150
|
+
...otherData,
|
|
151
|
+
},
|
|
302
152
|
} as CartFormInput;
|
|
303
153
|
}
|
|
304
154
|
|
|
305
|
-
// 把 ACTIONS / INPUT_NAME / getFormInput 挂到 CartForm 对象上 — 对齐 Hydrogen
|
|
306
155
|
const CartForm = CartFormBase as CartFormImpl;
|
|
307
156
|
CartForm.ACTIONS = CART_FORM_ACTIONS;
|
|
308
157
|
CartForm.INPUT_NAME = INPUT_NAME;
|
|
309
158
|
CartForm.getFormInput = getFormInput;
|
|
310
159
|
|
|
311
160
|
export { CartForm };
|
|
161
|
+
|
|
162
|
+
// useFetcher 透传 — 让商家也能用同款 API(其实就是 RR7 原生)
|
|
163
|
+
export { useFetcher } from 'react-router';
|
|
164
|
+
export type { FetcherWithComponents } from 'react-router';
|
|
165
|
+
|
|
166
|
+
// 为了 backward compat,保留旧 CartFormFetcher 别名
|
|
167
|
+
export type CartFormFetcher = FetcherWithComponents<any>;
|