best-unit 1.5.2 → 2.0.1

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.
@@ -0,0 +1,185 @@
1
+ import { useEffect, useState } from "preact/hooks";
2
+ import { Modal } from "@/components/common/modal";
3
+ import { t } from "@/local";
4
+ import { getPaymentDialogTheme } from "./theme";
5
+ // 线下提交逻辑已移动到 OfflinePayment 内部
6
+ import { SelectPayment } from "../select-payment";
7
+ import { OnlinePayment } from "../online-payment";
8
+ import { OfflinePayment } from "../offline-payment";
9
+ import { OfflineDetail } from "../offline-detail";
10
+
11
+ interface PaymentDialogProps {
12
+ visible: boolean;
13
+ onClose: () => void;
14
+ onSubmit: (form: {
15
+ amount: string;
16
+ rechargeChannel: string;
17
+ currency: string;
18
+ }) => Promise<void>;
19
+ }
20
+
21
+ export function PaymentDialog({
22
+ visible,
23
+ onClose,
24
+ onSubmit,
25
+ }: PaymentDialogProps) {
26
+ const theme = getPaymentDialogTheme();
27
+ const [isMobile, setIsMobile] = useState(false);
28
+
29
+ const [formState, setFormState] = useState({
30
+ currency: "USD",
31
+ amount: "",
32
+ channel: "",
33
+ paymentType: "online",
34
+ loading: false,
35
+ error: "",
36
+ amountError: "",
37
+ channelError: "",
38
+ showOfflineUpload: false, // 新增:是否显示银行转账回单上传页面
39
+ });
40
+
41
+ // 检测屏幕尺寸
42
+ useEffect(() => {
43
+ const checkMobile = () => {
44
+ setIsMobile(window.innerWidth <= 1020);
45
+ };
46
+
47
+ checkMobile();
48
+ window.addEventListener("resize", checkMobile);
49
+
50
+ return () => window.removeEventListener("resize", checkMobile);
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (!visible) {
55
+ setFormState({
56
+ currency: "USD",
57
+ amount: "",
58
+ channel: "",
59
+ paymentType: "online",
60
+ loading: false,
61
+ error: "",
62
+ amountError: "",
63
+ channelError: "",
64
+ showOfflineUpload: false,
65
+ });
66
+ }
67
+ }, [visible]);
68
+
69
+ // 处理渠道变化
70
+ const handleChannelChange = (channel: string, paymentType: string) => {
71
+ setFormState((s) => ({
72
+ ...s,
73
+ channel,
74
+ paymentType,
75
+ channelError: "",
76
+ }));
77
+ };
78
+
79
+ // 处理金额变化
80
+ const handleAmountChange = (amount: string) => {
81
+ setFormState((s) => ({ ...s, amount, amountError: "" }));
82
+ };
83
+
84
+ // 处理金额错误
85
+ const handleAmountError = (error: string) => {
86
+ setFormState((s) => ({ ...s, amountError: error }));
87
+ };
88
+
89
+ // 处理币种变化
90
+ const handleCurrencyChange = (currency: string) => {
91
+ setFormState((s) => ({ ...s, currency }));
92
+ };
93
+
94
+ // 处理在线支付提交
95
+ const handleOnlineSubmit = async (form: {
96
+ amount: string;
97
+ currency: string;
98
+ rechargeChannel: string;
99
+ }) => {
100
+ setFormState((s) => ({ ...s, loading: true, error: "" }));
101
+ try {
102
+ await onSubmit(form);
103
+ onClose();
104
+ } catch {
105
+ setFormState((s) => ({ ...s, error: t("提交失败,请重试") }));
106
+ } finally {
107
+ setFormState((s) => ({ ...s, loading: false }));
108
+ }
109
+ };
110
+
111
+ // 处理线下支付确认
112
+ const handleOfflineConfirm = () => {
113
+ console.log("用户确认银行转账详情");
114
+ // 切换到银行转账回单上传页面
115
+ setFormState((s) => ({ ...s, showOfflineUpload: true }));
116
+ };
117
+
118
+ // 处理银行转账回单上传:已在 OfflinePayment 内部直接提交
119
+
120
+ // 返回银行转账详情页面
121
+ const handleBackToOffline = () => {
122
+ setFormState((s) => ({ ...s, showOfflineUpload: false }));
123
+ };
124
+
125
+ return (
126
+ <Modal
127
+ visible={visible}
128
+ onClose={onClose}
129
+ title={t("在线支付")}
130
+ width={isMobile ? "95%" : 960}
131
+ maxWidth={isMobile ? "95%" : 1000}
132
+ >
133
+ <div
134
+ style={{
135
+ ...theme.wrapper,
136
+ ...(isMobile
137
+ ? {
138
+ gridTemplateColumns: "1fr",
139
+ gap: 16,
140
+ minWidth: "auto",
141
+ }
142
+ : {}),
143
+ }}
144
+ >
145
+ <SelectPayment
146
+ onChannelChange={handleChannelChange}
147
+ onAmountChange={handleAmountChange}
148
+ onCurrencyChange={handleCurrencyChange}
149
+ initialAmount={formState.amount}
150
+ initialCurrency={formState.currency}
151
+ initialPaymentType={formState.paymentType}
152
+ amountError={formState.amountError}
153
+ />
154
+
155
+ {formState.paymentType === "online" ? (
156
+ <OnlinePayment
157
+ onSubmit={handleOnlineSubmit}
158
+ amount={formState.amount}
159
+ currency={formState.currency}
160
+ channel={formState.channel}
161
+ loading={formState.loading}
162
+ error={formState.error}
163
+ onAmountError={handleAmountError}
164
+ />
165
+ ) : formState.showOfflineUpload ? (
166
+ <OfflinePayment
167
+ onBack={handleBackToOffline}
168
+ onCancel={onClose}
169
+ currency={formState.currency}
170
+ amount={formState.amount}
171
+ channel={formState.channel}
172
+ onAmountError={handleAmountError}
173
+ />
174
+ ) : (
175
+ <OfflineDetail
176
+ onConfirm={handleOfflineConfirm}
177
+ channelCode={formState.channel}
178
+ />
179
+ )}
180
+ </div>
181
+ </Modal>
182
+ );
183
+ }
184
+
185
+ export default PaymentDialog;
@@ -0,0 +1,305 @@
1
+ import { Size, Theme, type ThemeConfig } from "@/types";
2
+ import { getInitParams } from "@/utils/business";
3
+
4
+ function createThemes() {
5
+ const size = getInitParams<Size>("size");
6
+ const base = {
7
+ wrapper: {
8
+ display: "grid",
9
+ gridTemplateColumns: "1fr 1fr",
10
+ gap: size === Size.SMALL ? 12 : 20,
11
+ alignItems: "start",
12
+ minWidth: 880,
13
+ // 响应式设计
14
+ "@media (max-width: 1020px)": {
15
+ gridTemplateColumns: "1fr",
16
+ gap: 16,
17
+ minWidth: "auto",
18
+ width: "100%",
19
+ },
20
+ },
21
+ fieldLabel: {
22
+ fontSize: size === Size.SMALL ? 12 : 13,
23
+ marginBottom: 6,
24
+ fontWeight: 500,
25
+ textAlign: "left",
26
+ },
27
+ input: {
28
+ width: "100%",
29
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
30
+ borderRadius: 6,
31
+ outline: "none",
32
+ border: "1px solid",
33
+ boxSizing: "border-box",
34
+ paddingRight: size === Size.SMALL ? 48 : 64,
35
+ },
36
+ inputError: { border: "1px solid #ff4d4f" },
37
+ error: {
38
+ color: "#ff4d4f",
39
+ marginTop: 6,
40
+ fontSize: size === Size.SMALL ? 12 : 13,
41
+ },
42
+ inputWrapper: { position: "relative" },
43
+ inputSuffix: {
44
+ padding: "0 8px",
45
+ fontSize: size === Size.SMALL ? 12 : 13,
46
+ opacity: 0.8,
47
+ },
48
+ inputSuffixAbs: {
49
+ position: "absolute",
50
+ top: 0,
51
+ right: size === Size.SMALL ? 10 : 12,
52
+ height: "100%",
53
+ display: "flex",
54
+ alignItems: "center",
55
+ color: "#999",
56
+ pointerEvents: "none",
57
+ fontSize: size === Size.SMALL ? 12 : 13,
58
+ },
59
+ // 大标题(带左侧蓝色竖条)
60
+ blockTitle: {
61
+ fontSize: size === Size.SMALL ? 16 : 18,
62
+ fontWeight: 700,
63
+ marginBottom: size === Size.SMALL ? 12 : 16,
64
+ textAlign: "left",
65
+ display: "flex",
66
+ alignItems: "center",
67
+ gap: 8,
68
+ },
69
+ // 小标题
70
+ subTitle: {
71
+ marginTop: size === Size.SMALL ? 10 : 14,
72
+ marginBottom: size === Size.SMALL ? 8 : 10,
73
+ fontWeight: 600,
74
+ textAlign: "left",
75
+ },
76
+ titleBar: {
77
+ width: 3,
78
+ height: size === Size.SMALL ? 16 : 20,
79
+ background: "#1890ff",
80
+ borderRadius: 2,
81
+ },
82
+ methodList: {
83
+ display: "grid",
84
+ gridTemplateColumns: "1fr 1fr",
85
+ gap: 10,
86
+ // 响应式设计
87
+ "@media (max-width: 600px)": {
88
+ gridTemplateColumns: "1fr",
89
+ gap: 8,
90
+ },
91
+ },
92
+ bankList: {
93
+ display: "grid",
94
+ gridTemplateColumns: "1fr 1fr",
95
+ gap: 10,
96
+ // 响应式设计
97
+ "@media (max-width: 600px)": {
98
+ gridTemplateColumns: "1fr",
99
+ gap: 8,
100
+ },
101
+ },
102
+ } as const;
103
+
104
+ return {
105
+ white: {
106
+ ...base,
107
+ left: {
108
+ background: "#fff",
109
+ borderRadius: 8,
110
+ padding: 16,
111
+ border: "1px solid #E5E6EB",
112
+ },
113
+ right: { background: "#fff", borderRadius: 8, padding: 16 },
114
+ currencyRow: {
115
+ display: "flex",
116
+ gap: 10,
117
+ flexWrap: "wrap",
118
+ // 响应式设计
119
+ "@media (max-width: 480px)": {
120
+ gap: 8,
121
+ },
122
+ },
123
+ currencyItem: (active: boolean) => ({
124
+ padding: "6px 12px",
125
+ borderRadius: 6,
126
+ border: `1px solid ${active ? "#1890ff" : "#E5E6EB"}`,
127
+ color: active ? "#1890ff" : "#222",
128
+ background: active ? "#EEF7FF" : "#fff",
129
+ cursor: "pointer",
130
+ }),
131
+ input: {
132
+ ...base.input,
133
+ border: "1px solid #E5E6EB",
134
+ background: "#fff",
135
+ color: "#222",
136
+ },
137
+ payButton: {
138
+ width: "100%",
139
+ marginTop: size === Size.SMALL ? 10 : 14,
140
+ marginBottom: size === Size.SMALL ? 12 : 16,
141
+ background: "#1890ff",
142
+ color: "#fff",
143
+ border: "none",
144
+ borderRadius: 6,
145
+ padding: size === Size.SMALL ? "8px 12px" : "12px 16px",
146
+ fontWeight: 600,
147
+ cursor: "pointer",
148
+ },
149
+ methodItem: (active: boolean) => ({
150
+ padding: "10px 12px",
151
+ textAlign: "left",
152
+ borderRadius: 8,
153
+ border: `1px solid ${active ? "#1890ff" : "#F0F1F3"}`,
154
+ background: active ? "#EEF7FF" : "#F7F8FA",
155
+ cursor: "pointer",
156
+ }),
157
+ bankItem: {
158
+ padding: "10px 12px",
159
+ borderRadius: 8,
160
+ border: "1px solid #F0F1F3",
161
+ background: "#F7F8FA",
162
+ textAlign: "left",
163
+ },
164
+ noticeBox: {
165
+ marginTop: size === Size.SMALL ? 8 : 12,
166
+ marginBottom: size === Size.SMALL ? 12 : 16,
167
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
168
+ borderRadius: 6,
169
+ background: "#FFF7E6",
170
+ border: "1px solid #FFE7BA",
171
+ color: "#AD6800",
172
+ fontSize: size === Size.SMALL ? 12 : 13,
173
+ },
174
+ reminderTitle: { fontWeight: 600, margin: "6px 0", textAlign: "left" },
175
+ reminderList: {
176
+ margin: 0,
177
+ paddingLeft: 18,
178
+ color: "#6b7280",
179
+ lineHeight: 1.6,
180
+ textAlign: "left",
181
+ },
182
+ },
183
+ dark: {
184
+ ...base,
185
+ left: {
186
+ background: "#181A20",
187
+ borderRadius: 8,
188
+ padding: 16,
189
+ border: "1px solid #2B2E38",
190
+ },
191
+ right: {
192
+ background: "#181A20",
193
+ borderRadius: 8,
194
+ padding: 16,
195
+ border: "1px solid #2B2E38",
196
+ },
197
+ currencyRow: {
198
+ display: "flex",
199
+ gap: 10,
200
+ flexWrap: "wrap",
201
+ // 响应式设计
202
+ "@media (max-width: 480px)": {
203
+ gap: 8,
204
+ },
205
+ },
206
+ currencyItem: (active: boolean) => ({
207
+ padding: "6px 12px",
208
+ borderRadius: 6,
209
+ border: `1px solid ${active ? "#00E8C6" : "#374151"}`,
210
+ color: active ? "#00E8C6" : "#fff",
211
+ background: active ? "#0F2824" : "#23262F",
212
+ cursor: "pointer",
213
+ }),
214
+ input: {
215
+ ...base.input,
216
+ border: "1px solid #374151",
217
+ background: "#23262F",
218
+ color: "#fff",
219
+ },
220
+ payButton: {
221
+ width: "100%",
222
+ marginTop: size === Size.SMALL ? 10 : 14,
223
+ marginBottom: size === Size.SMALL ? 12 : 16,
224
+ background: "#00E8C6",
225
+ color: "#111",
226
+ border: "none",
227
+ borderRadius: 6,
228
+ padding: size === Size.SMALL ? "8px 12px" : "12px 16px",
229
+ fontWeight: 700,
230
+ cursor: "pointer",
231
+ },
232
+ methodItem: (active: boolean) => ({
233
+ padding: "10px 12px",
234
+ textAlign: "left",
235
+ borderRadius: 8,
236
+ border: `1px solid ${active ? "#00E8C6" : "#374151"}`,
237
+ background: active ? "#0F2824" : "#23262F",
238
+ color: "#fff",
239
+ cursor: "pointer",
240
+ }),
241
+ bankItem: {
242
+ padding: "10px 12px",
243
+ borderRadius: 8,
244
+ border: "1px solid #374151",
245
+ background: "#23262F",
246
+ textAlign: "left",
247
+ color: "#fff",
248
+ },
249
+ noticeBox: {
250
+ marginTop: size === Size.SMALL ? 8 : 12,
251
+ marginBottom: size === Size.SMALL ? 12 : 16,
252
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
253
+ borderRadius: 6,
254
+ background: "#2A1F0F",
255
+ border: "1px solid #5C3E12",
256
+ color: "#F4E3C1",
257
+ fontSize: size === Size.SMALL ? 12 : 13,
258
+ },
259
+ reminderTitle: {
260
+ fontWeight: 600,
261
+ margin: "6px 0",
262
+ textAlign: "left",
263
+ color: "#fff",
264
+ },
265
+ reminderList: {
266
+ margin: 0,
267
+ paddingLeft: 18,
268
+ color: "#B5B8BE",
269
+ lineHeight: 1.6,
270
+ textAlign: "left",
271
+ },
272
+ },
273
+ } as const;
274
+ }
275
+
276
+ export function getPaymentDialogTheme() {
277
+ const theme = getInitParams<Theme>("theme");
278
+ const themeConfig = getInitParams<ThemeConfig>("themeConfig");
279
+ const white = theme === Theme.WHITE;
280
+ const themes = createThemes();
281
+ const base = white ? themes.white : themes.dark;
282
+ if (themeConfig) {
283
+ const cfg = white ? themeConfig.white : themeConfig.dark;
284
+ if (cfg?.color) {
285
+ return {
286
+ ...base,
287
+ payButton: { ...base.payButton, background: cfg.color },
288
+ currencyItem: (active: boolean) => ({
289
+ ...base.currencyItem(active),
290
+ border: `1px solid ${
291
+ active ? cfg.color : white ? "#E5E6EB" : "#374151"
292
+ }`,
293
+ color: active ? cfg.color : white ? "#222" : "#fff",
294
+ }),
295
+ methodItem: (active: boolean) => ({
296
+ ...base.methodItem(active),
297
+ border: `1px solid ${
298
+ active ? cfg.color : white ? "#F0F1F3" : "#374151"
299
+ }`,
300
+ }),
301
+ } as any;
302
+ }
303
+ }
304
+ return base as any;
305
+ }
@@ -0,0 +1,192 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import { t } from "@/local";
3
+ import { getSelectPaymentTheme } from "./theme";
4
+ import { getOfflineChannelMap } from "@/api";
5
+
6
+ interface SelectPaymentProps {
7
+ onChannelChange: (channel: string, paymentType: string) => void;
8
+ onAmountChange: (amount: string) => void;
9
+ onCurrencyChange: (currency: string) => void;
10
+ initialAmount?: string;
11
+ initialCurrency?: string;
12
+ initialPaymentType?: string;
13
+ amountError?: string;
14
+ }
15
+
16
+ export function SelectPayment({
17
+ onChannelChange,
18
+ onAmountChange,
19
+ onCurrencyChange,
20
+ initialAmount = "",
21
+ initialCurrency = "USD",
22
+ initialPaymentType = "online",
23
+ amountError = "",
24
+ }: SelectPaymentProps) {
25
+ const theme = getSelectPaymentTheme();
26
+
27
+ // 币种从本地字典读取;渠道通过接口获取
28
+ const allDicts = JSON.parse(sessionStorage.getItem("all_dicts") || "{}");
29
+ const currencyDict = allDicts?.currency || [];
30
+ const [onlineChannelDict, setOnlineChannelDict] = useState<any[]>([]);
31
+ const [bankChannelDict, setBankChannelDict] = useState<any[]>([]);
32
+
33
+ const [formState, setFormState] = useState({
34
+ currency: initialCurrency,
35
+ amount: initialAmount,
36
+ onlineChannel: "",
37
+ bankChannel: "",
38
+ paymentType: initialPaymentType,
39
+ amountError: "",
40
+ });
41
+
42
+ // 根据币种拉取线上/线下渠道映射,初始化默认选项
43
+ useEffect(() => {
44
+ getOfflineChannelMap({ currency: formState.currency }).then((res) => {
45
+ const online = res.onlineChannel || [];
46
+ const offline = res.offlineChannel || [];
47
+ setOnlineChannelDict(online);
48
+ setBankChannelDict(offline);
49
+
50
+ // 是否展示线上支付
51
+ const showOnline = online.length > 0;
52
+ const defaultBank = offline?.[0]?.value || "";
53
+ const defaultOnline = online?.[0]?.value || "";
54
+ const paymentType = showOnline ? initialPaymentType : "bank";
55
+ const channelValue =
56
+ paymentType === "online" ? defaultOnline : defaultBank;
57
+
58
+ setFormState((s) => ({
59
+ ...s,
60
+ onlineChannel: defaultOnline,
61
+ bankChannel: defaultBank,
62
+ paymentType,
63
+ }));
64
+ if (channelValue) onChannelChange(channelValue, paymentType);
65
+ });
66
+ }, [formState.currency]);
67
+
68
+ // 格式化金额输入
69
+ function formatAmountInput(value: string) {
70
+ value = value.replace(/[^\d.]/g, "");
71
+ value = value.replace(/\.(?=.*\.)/g, "");
72
+ value = value.replace(/^(\d+)(\.\d{0,2})?.*$/, "$1$2");
73
+ value = value.replace(/^0+(\d)/, "$1");
74
+ if (value.startsWith(".")) value = "0" + value;
75
+ return value;
76
+ }
77
+
78
+ // 处理币种变化
79
+ const handleCurrencyChange = (currency: string) => {
80
+ setFormState((s) => ({ ...s, currency }));
81
+ onCurrencyChange(currency);
82
+ };
83
+
84
+ // 处理金额变化
85
+ const handleAmountChange = (amount: string) => {
86
+ const formattedAmount = formatAmountInput(amount);
87
+ setFormState((s) => ({ ...s, amount: formattedAmount, amountError: "" }));
88
+ onAmountChange(formattedAmount);
89
+ };
90
+
91
+ // 处理渠道变化
92
+ const handleChannelChange = (channel: string, paymentType: string) => {
93
+ setFormState((s) => ({
94
+ ...s,
95
+ [paymentType === "online" ? "onlineChannel" : "bankChannel"]: channel,
96
+ paymentType,
97
+ }));
98
+ onChannelChange(channel, paymentType);
99
+ };
100
+ return (
101
+ <div style={theme.left}>
102
+ <div style={theme.blockTitle}>
103
+ <span style={theme.titleBar} />
104
+ {t("选择支付方式")}
105
+ </div>
106
+ <div style={theme.fieldLabel}>{t("支付币种")}</div>
107
+ <div style={theme.currencyRow}>
108
+ {(currencyDict || []).slice(0, 4).map((c: any) => (
109
+ <button
110
+ type="button"
111
+ onClick={() => handleCurrencyChange(c.value)}
112
+ style={theme.currencyItem(formState.currency === c.value)}
113
+ >
114
+ {c.label}
115
+ </button>
116
+ ))}
117
+ </div>
118
+
119
+ <div style={{ height: 12 }} />
120
+ <div style={theme.fieldLabel}>
121
+ <span style={theme.required}>*</span>
122
+ {t("支付金额")}
123
+ </div>
124
+ <div style={theme.inputWrapper}>
125
+ <input
126
+ type="text"
127
+ placeholder={t("请输入充值金额")}
128
+ value={formState.amount}
129
+ onInput={(e) => {
130
+ handleAmountChange((e.target as HTMLInputElement).value);
131
+ }}
132
+ style={{
133
+ ...theme.input,
134
+ ...(amountError ? theme.inputError : {}),
135
+ }}
136
+ />
137
+ <div style={theme.inputSuffixAbs}>{formState.currency}</div>
138
+ </div>
139
+ {amountError && <div style={theme.error}>{amountError}</div>}
140
+
141
+ <div style={{ height: 20 }} />
142
+ <div style={theme.fieldLabel}>{t("支付方式")}</div>
143
+ {onlineChannelDict.length > 0 && (
144
+ <div>
145
+ <div style={theme.subTitle}>{t("线上支付")}</div>
146
+ <div style={theme.methodList}>
147
+ {onlineChannelDict.map((item: any) => (
148
+ <button
149
+ type="button"
150
+ onClick={() => handleChannelChange(item.value, "online")}
151
+ style={theme.methodItem(
152
+ formState.onlineChannel === item.value &&
153
+ formState.paymentType === "online"
154
+ )}
155
+ >
156
+ {item.icon && (
157
+ <img
158
+ src={item.icon}
159
+ alt={item.label}
160
+ style={theme.channelIcon}
161
+ />
162
+ )}
163
+ {item.label}
164
+ </button>
165
+ ))}
166
+ </div>
167
+ </div>
168
+ )}
169
+
170
+ <div style={theme.subTitle}>{t("银行转账")}</div>
171
+ <div style={theme.bankList}>
172
+ {bankChannelDict.map((item: any) => (
173
+ <button
174
+ type="button"
175
+ onClick={() => handleChannelChange(item.value, "bank")}
176
+ style={theme.bankItem(
177
+ formState.bankChannel === item.value &&
178
+ formState.paymentType === "bank"
179
+ )}
180
+ >
181
+ {item.icon && (
182
+ <img src={item.icon} alt={item.label} style={theme.channelIcon} />
183
+ )}
184
+ {item.label}
185
+ </button>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ export default SelectPayment;