best-unit 1.0.0 → 1.0.3

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 (48) hide show
  1. package/README.md +1 -0
  2. package/dist/best-unit.cjs +60 -0
  3. package/dist/best-unit.js +3988 -0
  4. package/dist/types/global.d.ts +64 -0
  5. package/dist/types/index.ts +17 -0
  6. package/dist/types/preact-custom-element.d.ts +1 -0
  7. package/index.html +2 -2
  8. package/package.json +14 -7
  9. package/src/api/axiosInstance.ts +94 -0
  10. package/src/api/index.ts +118 -15
  11. package/src/api/proxy.ts +11 -0
  12. package/src/components/business/recharge-sdk/components/Button.tsx +42 -0
  13. package/src/components/business/recharge-sdk/components/OfflineTransferForm.tsx +365 -0
  14. package/src/components/business/recharge-sdk/components/OnlineRechargeForm.tsx +389 -0
  15. package/src/components/business/recharge-sdk/components/Recharge.tsx +288 -0
  16. package/src/components/business/recharge-sdk/index.tsx +39 -0
  17. package/src/components/business/statistical-balance/index.tsx +206 -0
  18. package/src/components/common/HoverPopover.tsx +215 -0
  19. package/src/components/common/Message.tsx +324 -0
  20. package/src/components/common/Select.tsx +278 -0
  21. package/src/components/common/Upload.tsx +207 -0
  22. package/src/demo/App.tsx +429 -0
  23. package/src/demo/index.tsx +4 -0
  24. package/src/local/en.ts +61 -0
  25. package/src/local/index.ts +36 -0
  26. package/src/local/zh.ts +60 -0
  27. package/src/main.ts +10 -0
  28. package/src/types/global.d.ts +64 -0
  29. package/src/types/index.ts +17 -0
  30. package/src/types/preact-custom-element.d.ts +1 -0
  31. package/src/utils/business/index.ts +48 -0
  32. package/src/utils/common/index.ts +8 -0
  33. package/src/vite-env.d.ts +1 -0
  34. package/tsconfig.app.json +31 -0
  35. package/tsconfig.json +15 -0
  36. package/tsconfig.node.json +24 -0
  37. package/vite.config.ts +18 -0
  38. package/example/index.html +0 -24
  39. package/public/vite.svg +0 -1
  40. package/src/App.jsx +0 -6
  41. package/src/api/define-api.ts +0 -49
  42. package/src/javascript.svg +0 -1
  43. package/src/main.jsx +0 -4
  44. package/src/sdk/index.ts +0 -33
  45. package/src/style.css +0 -96
  46. package/src/views/AccountModal.jsx +0 -125
  47. package/src/views/Home.jsx +0 -53
  48. package/vite.config.js +0 -31
@@ -0,0 +1,389 @@
1
+ import type { FunctionalComponent } from "preact";
2
+ import { useState, useEffect } from "preact/hooks";
3
+ import { t } from "../../../../local";
4
+ import { Theme } from "../../../../types";
5
+ import { Select } from "../../../common/Select";
6
+ import { calcPaymentAmount } from "../../../../api";
7
+
8
+ interface OnlineRechargeFormProps {
9
+ formState: {
10
+ amount: string;
11
+ rechargeChannel: string;
12
+ currency: string;
13
+ loading: boolean;
14
+ error: string;
15
+ amountError: string;
16
+ rechargeChannelError: string;
17
+ currencyError: string;
18
+ };
19
+ setFormState: (fn: (state: any) => any) => void;
20
+ onClose: () => void;
21
+ loading: boolean;
22
+ }
23
+
24
+ // 辅助函数:只允许输入数字和小数点,且最多两位小数
25
+ function formatAmountInput(value: string) {
26
+ // 只保留数字和小数点
27
+ value = value.replace(/[^\d.]/g, "");
28
+ // 只保留第一个小数点
29
+ value = value.replace(/\.(?=.*\.)/g, "");
30
+ // 保证最多两位小数
31
+ value = value.replace(/^(\d+)(\.\d{0,2})?.*$/, "$1$2");
32
+ // 去除前导0(保留0.和0.xx)
33
+ value = value.replace(/^0+(\d)/, "$1");
34
+ if (value.startsWith(".")) value = "0" + value;
35
+ return value;
36
+ }
37
+ // 辅助函数:格式化为两位小数
38
+ function formatToTwoDecimal(value: string) {
39
+ if (!value) return "";
40
+ const num = parseFloat(value);
41
+ if (isNaN(num)) return "";
42
+ return num.toFixed(2);
43
+ }
44
+
45
+ export const OnlineRechargeForm: FunctionalComponent<
46
+ OnlineRechargeFormProps
47
+ > = ({ formState, setFormState, onClose, loading }) => {
48
+ const allDicts = JSON.parse(sessionStorage.getItem("all_dicts") || "{}");
49
+ const currencyDict = allDicts?.currency || [];
50
+ const channelDict =
51
+ allDicts?.channel?.filter((item: any) => item.payment_support) || [];
52
+ const fundUnitParams = JSON.parse(
53
+ sessionStorage.getItem("fund_unit_params") || "{}"
54
+ );
55
+ const whiteTheme = fundUnitParams.theme === Theme.WHITE;
56
+ const [actualAmount, setActualAmount] = useState<string>("");
57
+ const [showFeeTip, setShowFeeTip] = useState(false);
58
+
59
+ // 当三个参数都填写完整时,计算实际支付金额
60
+ useEffect(() => {
61
+ if (formState.currency && formState.amount && formState.rechargeChannel) {
62
+ calcPaymentAmount({
63
+ channel: formState.rechargeChannel,
64
+ amount: formState.amount,
65
+ currency: formState.currency,
66
+ })
67
+ .then((paymentAmount) => {
68
+ setActualAmount(paymentAmount);
69
+ setShowFeeTip(true);
70
+ })
71
+ .catch((error) => {
72
+ console.error("计算支付金额失败:", error);
73
+ setShowFeeTip(false);
74
+ });
75
+ } else {
76
+ setShowFeeTip(false);
77
+ }
78
+ }, [formState.currency, formState.amount, formState.rechargeChannel]);
79
+
80
+ const theme = whiteTheme
81
+ ? {
82
+ label: {
83
+ marginBottom: 8,
84
+ fontSize: 14,
85
+ color: "#222",
86
+ textAlign: "left",
87
+ display: "block",
88
+ },
89
+ input: {
90
+ width: "100%",
91
+ padding: "10px 12px",
92
+ borderRadius: 6,
93
+ boxSizing: "border-box",
94
+ border: "1px solid #E5E6EB",
95
+ background: "#fff",
96
+ color: "#222",
97
+ fontSize: 15,
98
+ outline: "none",
99
+ marginBottom: 0,
100
+ },
101
+ inputError: {
102
+ border: "1px solid #ff4d4f",
103
+ },
104
+ selectError: {
105
+ border: "1px solid #ff4d4f",
106
+ },
107
+ error: {
108
+ color: "#ff4d4f",
109
+ fontSize: 13,
110
+ marginTop: 4,
111
+ textAlign: "left",
112
+ },
113
+ buttonCancel: {
114
+ background: "#fff",
115
+ color: "#222",
116
+ border: "1px solid #E5E6EB",
117
+ borderRadius: 6,
118
+ padding: "8px 24px",
119
+ fontSize: 15,
120
+ cursor: "pointer",
121
+ },
122
+ buttonSubmit: {
123
+ background: "#1890ff",
124
+ color: "#fff",
125
+ border: "none",
126
+ borderRadius: 6,
127
+ padding: "8px 24px",
128
+ fontSize: 15,
129
+ cursor: "pointer",
130
+ fontWeight: 600,
131
+ },
132
+ }
133
+ : {
134
+ label: {
135
+ marginBottom: 8,
136
+ fontSize: 14,
137
+ color: "#fff",
138
+ textAlign: "left",
139
+ display: "block",
140
+ },
141
+ input: {
142
+ width: "100%",
143
+ padding: "10px 12px",
144
+ borderRadius: 6,
145
+ boxSizing: "border-box",
146
+ border: "1px solid #23262F",
147
+ background: "#23262F",
148
+ color: "#fff",
149
+ fontSize: 15,
150
+ outline: "none",
151
+ marginBottom: 0,
152
+ },
153
+ inputError: {
154
+ border: "1px solid #ff4d4f",
155
+ },
156
+ selectError: {
157
+ border: "1px solid #ff4d4f",
158
+ },
159
+ error: {
160
+ color: "#ff4d4f",
161
+ fontSize: 13,
162
+ marginTop: 4,
163
+ textAlign: "left",
164
+ },
165
+ buttonCancel: {
166
+ background: "#23262F",
167
+ color: "#fff",
168
+ border: "none",
169
+ borderRadius: 6,
170
+ padding: "8px 24px",
171
+ fontSize: 15,
172
+ cursor: "pointer",
173
+ },
174
+ buttonSubmit: {
175
+ background: "#00E8C6",
176
+ color: "#fff",
177
+ border: "none",
178
+ borderRadius: 6,
179
+ padding: "8px 24px",
180
+ fontSize: 15,
181
+ cursor: "pointer",
182
+ fontWeight: 600,
183
+ },
184
+ };
185
+ // const selectCurrencyStyle = whiteTheme
186
+ // ? {
187
+ // width: "100%",
188
+ // padding: "10px 12px",
189
+ // borderRadius: 6,
190
+ // border: "1px solid #E5E6EB",
191
+ // background: "#fff",
192
+ // color: "#222",
193
+ // fontSize: 15,
194
+ // outline: "none",
195
+ // }
196
+ // : {
197
+ // width: "100%",
198
+ // padding: "10px 12px",
199
+ // borderRadius: 6,
200
+ // border: "1px solid #23262F",
201
+ // background: "#23262F",
202
+ // color: "#fff",
203
+ // fontSize: 15,
204
+ // outline: "none",
205
+ // };
206
+ // const labelStyle = whiteTheme
207
+ // ? {
208
+ // marginBottom: 8,
209
+ // fontSize: 14,
210
+ // color: "#222",
211
+ // textAlign: "left",
212
+ // display: "block",
213
+ // }
214
+ // : { marginBottom: 8, fontSize: 14, textAlign: "left", display: "block" };
215
+ // const errorStyle = { color: "#ff4d4f", fontSize: 13, marginTop: 4 };
216
+ const buttonCancelStyle = whiteTheme
217
+ ? {
218
+ background: "#fff",
219
+ color: "#222",
220
+ border: "1px solid #E5E6EB",
221
+ borderRadius: 6,
222
+ padding: "8px 24px",
223
+ fontSize: 15,
224
+ cursor: "pointer",
225
+ }
226
+ : {
227
+ background: "#23262F",
228
+ color: "#fff",
229
+ border: "none",
230
+ borderRadius: 6,
231
+ padding: "8px 24px",
232
+ fontSize: 15,
233
+ cursor: "pointer",
234
+ };
235
+ const buttonSubmitStyle = whiteTheme
236
+ ? {
237
+ background: "#1890ff",
238
+ color: "#fff",
239
+ border: "none",
240
+ borderRadius: 6,
241
+ padding: "8px 24px",
242
+ fontSize: 15,
243
+ cursor: "pointer",
244
+ fontWeight: 600,
245
+ }
246
+ : {
247
+ background: "#00E8C6",
248
+ color: "#fff",
249
+ border: "none",
250
+ borderRadius: 6,
251
+ padding: "8px 24px",
252
+ fontSize: 15,
253
+ cursor: "pointer",
254
+ fontWeight: 600,
255
+ };
256
+
257
+ return (
258
+ <>
259
+ <div style={{ marginBottom: 18 }}>
260
+ <div style={theme.label}>
261
+ <span style={{ color: "#F53F3F" }}>*</span> {t("充值币种")}
262
+ </div>
263
+ <Select
264
+ value={formState.currency}
265
+ onChange={(value) => {
266
+ setFormState((state: any) => ({
267
+ ...state,
268
+ currency: value,
269
+ }));
270
+ }}
271
+ options={currencyDict?.map((item: any) => ({
272
+ value: item.value,
273
+ label: item.label,
274
+ }))}
275
+ placeholder={t("请选择充值币种")}
276
+ />
277
+ </div>
278
+ <div style={{ marginBottom: 18 }}>
279
+ <div style={theme.label}>
280
+ <span style={{ color: "#F53F3F" }}>*</span> {t("充值金额")}
281
+ </div>
282
+ <input
283
+ type="text"
284
+ placeholder={t("请输入充值金额")}
285
+ value={formState.amount}
286
+ onInput={(e) => {
287
+ let value = (e.target as HTMLInputElement).value;
288
+ value = formatAmountInput(value);
289
+ let amountError = "";
290
+ // 只有输入为合法数字且不以小数点结尾时才做区间修正
291
+ if (value && !value.endsWith(".")) {
292
+ let num = parseFloat(value);
293
+ if (!isNaN(num)) {
294
+ if (num < 1) num = 1;
295
+ if (num > 999999.99) num = 999999.99;
296
+ value = num.toString();
297
+ // 如果原始输入有小数点且小数点后有内容,保留小数部分
298
+ if (/\./.test((e.target as HTMLInputElement).value)) {
299
+ const decimalPart = (
300
+ e.target as HTMLInputElement
301
+ ).value.split(".")[1];
302
+ if (decimalPart !== undefined && decimalPart.length > 0) {
303
+ value = num.toFixed(Math.min(decimalPart.length, 2));
304
+ }
305
+ }
306
+ }
307
+ }
308
+ setFormState((state: any) => ({
309
+ ...state,
310
+ amount: value,
311
+ amountError,
312
+ }));
313
+ }}
314
+ onBlur={(e) => {
315
+ let value = (e.target as HTMLInputElement).value;
316
+ value = formatToTwoDecimal(value);
317
+ setFormState((state: any) => ({
318
+ ...state,
319
+ amount: value,
320
+ amountError: "",
321
+ }));
322
+ }}
323
+ style={{
324
+ ...theme.input,
325
+ ...(formState.amountError ? theme.inputError : {}),
326
+ }}
327
+ />
328
+ {formState.amountError && (
329
+ <div style={theme.error}>{formState.amountError}</div>
330
+ )}
331
+ </div>
332
+ <div style={{ marginBottom: 24 }}>
333
+ <div style={theme.label}>
334
+ <span style={{ color: "#F53F3F" }}>*</span> {t("支付平台")}
335
+ </div>
336
+ <Select
337
+ value={formState.rechargeChannel}
338
+ onChange={(value) => {
339
+ setFormState((state: any) => ({
340
+ ...state,
341
+ rechargeChannel: value,
342
+ rechargeChannelError: value ? "" : state.rechargeChannelError,
343
+ }));
344
+ }}
345
+ options={channelDict?.map((item: any) => ({
346
+ value: item.value,
347
+ label: item.label,
348
+ }))}
349
+ placeholder={t("请选择支付平台")}
350
+ ></Select>
351
+ {formState.rechargeChannelError && (
352
+ <div style={theme.error}>{formState.rechargeChannelError}</div>
353
+ )}
354
+ </div>
355
+ {/* 手续费提示 */}
356
+ {showFeeTip && actualAmount && (
357
+ <div
358
+ style={{
359
+ marginBottom: 24,
360
+ padding: "12px 16px",
361
+ background: whiteTheme ? "#f6ffed" : "#1a1a1a",
362
+ border: `1px solid ${whiteTheme ? "#b7eb8f" : "#333"}`,
363
+ borderRadius: 6,
364
+ fontSize: 14,
365
+ color: whiteTheme ? "#52c41a" : "#52c41a",
366
+ }}
367
+ >
368
+ {channelDict.find(
369
+ (item: any) => item.value === formState.rechargeChannel
370
+ )?.label || formState.rechargeChannel}
371
+ {t("需要收取手续费,实际支付金额约为:")}${actualAmount}
372
+ </div>
373
+ )}
374
+ {formState.error && (
375
+ <div style={{ color: "#ff4d4f", marginBottom: 12 }}>
376
+ {formState.error}
377
+ </div>
378
+ )}
379
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: 12 }}>
380
+ <button type="button" onClick={onClose} style={buttonCancelStyle}>
381
+ {t("取消")}
382
+ </button>
383
+ <button type="submit" disabled={loading} style={buttonSubmitStyle}>
384
+ {loading ? t("提交中...") : t("去支付")}
385
+ </button>
386
+ </div>
387
+ </>
388
+ );
389
+ };
@@ -0,0 +1,288 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import { OnlineRechargeForm } from "./OnlineRechargeForm";
3
+ import { OfflineTransferForm } from "./OfflineTransferForm";
4
+ import { t } from "../../../../local";
5
+ import { Theme } from "../../../../types";
6
+
7
+ interface ModalFormProps {
8
+ visible: boolean;
9
+ onClose: () => void;
10
+ onSubmit: (form: {
11
+ amount: string;
12
+ rechargeChannel: string;
13
+ currency: string;
14
+ }) => Promise<void>;
15
+ color?: string;
16
+ merchantId?: string;
17
+ bizType?: string;
18
+ token?: string;
19
+ }
20
+
21
+ export function Recharge({ visible, onClose, onSubmit }: ModalFormProps) {
22
+ const [formState, setFormState] = useState({
23
+ amount: "",
24
+ rechargeChannel: "",
25
+ currency: "USD",
26
+ loading: false,
27
+ error: "",
28
+ amountError: "",
29
+ rechargeChannelError: "",
30
+ currencyError: "",
31
+ });
32
+ const [activeTab, setActiveTab] = useState<"online" | "offline">("online");
33
+ const [offlineFormState, setOfflineFormState] = useState({
34
+ platform: "",
35
+ transactionId: "",
36
+ files: [],
37
+ platformError: "",
38
+ transactionIdError: "",
39
+ filesError: "",
40
+ loading: false,
41
+ });
42
+
43
+ // 每次关闭弹窗时重置内容
44
+ useEffect(() => {
45
+ if (!visible) {
46
+ setActiveTab("online");
47
+ setFormState({
48
+ amount: "",
49
+ rechargeChannel: "",
50
+ currency: "USD",
51
+ loading: false,
52
+ error: "",
53
+ amountError: "",
54
+ rechargeChannelError: "",
55
+ currencyError: "",
56
+ });
57
+ setOfflineFormState({
58
+ platform: "",
59
+ transactionId: "",
60
+ files: [],
61
+ platformError: "",
62
+ transactionIdError: "",
63
+ filesError: "",
64
+ loading: false,
65
+ });
66
+ }
67
+ }, [visible]);
68
+
69
+ if (!visible) return null;
70
+
71
+ const handleSubmit = async (e: Event) => {
72
+ e.preventDefault();
73
+ let valid = true;
74
+ setFormState((state) => ({
75
+ ...state,
76
+ amountError: "",
77
+ rechargeChannelError: "",
78
+ }));
79
+ if (!formState.amount.trim()) {
80
+ setFormState((state) => ({ ...state, amountError: t("请输入充值金额") }));
81
+ valid = false;
82
+ }
83
+ if (!formState.rechargeChannel) {
84
+ setFormState((state) => ({
85
+ ...state,
86
+ rechargeChannelError: t("请选择支付平台"),
87
+ }));
88
+ valid = false;
89
+ }
90
+ if (!valid) return;
91
+ setFormState((state) => ({ ...state, loading: true, error: "" }));
92
+ try {
93
+ await onSubmit({
94
+ amount: formState.amount,
95
+ rechargeChannel: formState.rechargeChannel,
96
+ currency: formState.currency,
97
+ });
98
+ onClose();
99
+ } catch {
100
+ setFormState((state) => ({ ...state, error: t("提交失败,请重试") }));
101
+ } finally {
102
+ setFormState((state) => ({ ...state, loading: false }));
103
+ }
104
+ };
105
+
106
+ // 记录鼠标是否在mask上按下
107
+ const [maskMouseDown, setMaskMouseDown] = useState(false);
108
+
109
+ // 只有mousedown和mouseup都在mask上才关闭弹窗
110
+ const handleMaskMouseDown = (e: any) => {
111
+ if (e.target === e.currentTarget) {
112
+ setMaskMouseDown(true);
113
+ } else {
114
+ setMaskMouseDown(false);
115
+ }
116
+ };
117
+ const handleMaskMouseUp = (e: any) => {
118
+ if (e.target === e.currentTarget && maskMouseDown) {
119
+ onClose();
120
+ }
121
+ setMaskMouseDown(false);
122
+ };
123
+
124
+ const fundUnitParams = JSON.parse(
125
+ sessionStorage.getItem("fund_unit_params") || "{}"
126
+ );
127
+ const whiteTheme = fundUnitParams.theme === Theme.WHITE;
128
+
129
+ const theme = whiteTheme
130
+ ? {
131
+ modalBg: "#fff",
132
+ modalColor: "#222",
133
+ modalBoxShadow: "0 4px 24px rgba(0,0,0,0.08)",
134
+ mask: "rgba(0,0,0,0.3)",
135
+ title: {
136
+ fontWeight: 600,
137
+ fontSize: 20,
138
+ marginBottom: 24,
139
+ textAlign: "left",
140
+ color: "#222",
141
+ },
142
+ closeBtn: {
143
+ position: "absolute",
144
+ right: 16,
145
+ top: 16,
146
+ background: "none",
147
+ border: "none",
148
+ color: "#222",
149
+ fontSize: 22,
150
+ cursor: "pointer",
151
+ lineHeight: 1,
152
+ },
153
+ tabBtn: (active: boolean, left: boolean) => ({
154
+ flex: 1,
155
+ background: active ? "#fff" : "#F7F8FA",
156
+ color: active ? "#1890ff" : "#222",
157
+ border: "none",
158
+ borderRadius: left ? "8px 0 0 8px" : "0 8px 8px 0",
159
+ fontWeight: active ? 600 : 400,
160
+ fontSize: 16,
161
+ height: 48,
162
+ boxShadow: active ? "0 2px 8px 0 rgba(20,20,20,0.04)" : "none",
163
+ outline: "none",
164
+ cursor: "pointer",
165
+ borderRight: left ? "1px solid #F0F1F3" : undefined,
166
+ borderLeft: !left ? "1px solid #F0F1F3" : undefined,
167
+ transition: "all 0.2s",
168
+ }),
169
+ }
170
+ : {
171
+ modalBg: "#181A20",
172
+ modalColor: "#fff",
173
+ modalBoxShadow: "0 4px 24px rgba(0,0,0,0.5)",
174
+ mask: "rgba(0,0,0,0.7)",
175
+ title: {
176
+ fontWeight: 600,
177
+ fontSize: 20,
178
+ marginBottom: 24,
179
+ textAlign: "left",
180
+ color: "#fff",
181
+ },
182
+ closeBtn: {
183
+ position: "absolute",
184
+ right: 16,
185
+ top: 16,
186
+ background: "none",
187
+ border: "none",
188
+ color: "#fff",
189
+ fontSize: 22,
190
+ cursor: "pointer",
191
+ lineHeight: 1,
192
+ },
193
+ tabBtn: (active: boolean, left: boolean) => ({
194
+ flex: 1,
195
+ background: active ? "#23262F" : "#181A20",
196
+ color: active ? "#00E8C6" : "#fff",
197
+ border: "none",
198
+ borderRadius: left ? "8px 0 0 8px" : "0 8px 8px 0",
199
+ fontWeight: active ? 600 : 400,
200
+ fontSize: 16,
201
+ height: 48,
202
+ boxShadow: active ? "0 2px 8px 0 rgba(20,20,20,0.10)" : "none",
203
+ outline: "none",
204
+ cursor: "pointer",
205
+ borderRight: left ? "1px solid #23262F" : undefined,
206
+ borderLeft: !left ? "1px solid #23262F" : undefined,
207
+ transition: "all 0.2s",
208
+ }),
209
+ };
210
+
211
+ return (
212
+ <div
213
+ style={{
214
+ position: "fixed",
215
+ top: 0,
216
+ left: 0,
217
+ right: 0,
218
+ bottom: 0,
219
+ background: theme.mask,
220
+ display: "flex",
221
+ alignItems: "center",
222
+ justifyContent: "center",
223
+ zIndex: 9999,
224
+ }}
225
+ onMouseDown={handleMaskMouseDown}
226
+ onMouseUp={handleMaskMouseUp}
227
+ >
228
+ <form
229
+ onSubmit={handleSubmit}
230
+ style={{
231
+ background: theme.modalBg,
232
+ padding: 32,
233
+ borderRadius: 12,
234
+ minWidth: 400,
235
+ maxWidth: 400,
236
+ color: theme.modalColor,
237
+ boxShadow: theme.modalBoxShadow,
238
+ position: "relative",
239
+ }}
240
+ onClick={(e) => e.stopPropagation()}
241
+ >
242
+ {/* 关闭按钮 */}
243
+ <button
244
+ type="button"
245
+ onClick={onClose}
246
+ style={theme.closeBtn}
247
+ aria-label={t("关闭")}
248
+ >
249
+ ×
250
+ </button>
251
+ <div style={theme.title}>{t("充值 / 转账")}</div>
252
+ {/* tab 按钮区域 */}
253
+ <div style={{ display: "flex", marginBottom: 28 }}>
254
+ <button
255
+ type="button"
256
+ onClick={() => setActiveTab("online")}
257
+ style={theme.tabBtn(activeTab === "online", true)}
258
+ >
259
+ {t("在线充值")}
260
+ </button>
261
+ <button
262
+ type="button"
263
+ onClick={() => setActiveTab("offline")}
264
+ style={theme.tabBtn(activeTab === "offline", false)}
265
+ >
266
+ {t("线下转账")}
267
+ </button>
268
+ </div>
269
+ {/* tab 内容区域 */}
270
+ {activeTab === "online" ? (
271
+ <OnlineRechargeForm
272
+ formState={formState}
273
+ setFormState={setFormState}
274
+ onClose={onClose}
275
+ loading={formState.loading}
276
+ />
277
+ ) : (
278
+ <OfflineTransferForm
279
+ formState={offlineFormState}
280
+ setFormState={setOfflineFormState}
281
+ onClose={onClose}
282
+ loading={offlineFormState.loading}
283
+ />
284
+ )}
285
+ </form>
286
+ </div>
287
+ );
288
+ }