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,289 @@
1
+ import { Size, Theme, type ThemeConfig } from "@/types";
2
+ import { getInitParams } from "@/utils/business";
3
+
4
+ function createOfflinePaymentThemes() {
5
+ const size = getInitParams<Size>("size");
6
+ const base = {
7
+ container: {
8
+ background: "#fff",
9
+ borderRadius: 8,
10
+ padding: size === Size.SMALL ? 16 : 20,
11
+ width: "100%",
12
+ maxWidth: "100%",
13
+ boxSizing: "border-box",
14
+ // 响应式设计
15
+ "@media (max-width: 1020px)": {
16
+ padding: size === Size.SMALL ? 12 : 16,
17
+ },
18
+ },
19
+ header: {
20
+ marginBottom: size === Size.SMALL ? 20 : 24,
21
+ },
22
+ backButton: {
23
+ display: "flex",
24
+ alignItems: "center",
25
+ gap: 8,
26
+ background: "none",
27
+ border: "none",
28
+ color: "#1890ff",
29
+ cursor: "pointer",
30
+ fontSize: size === Size.SMALL ? 14 : 16,
31
+ fontWeight: 500,
32
+ },
33
+ backIcon: {
34
+ fontSize: size === Size.SMALL ? 16 : 18,
35
+ },
36
+ uploadSection: {
37
+ marginBottom: size === Size.SMALL ? 24 : 32,
38
+ },
39
+ uploadArea: {
40
+ border: "2px dashed #D9D9D9",
41
+ borderRadius: 8,
42
+ padding: size === Size.SMALL ? "40px 20px" : "60px 30px",
43
+ textAlign: "center",
44
+ cursor: "pointer",
45
+ background: "#FAFAFA",
46
+ transition: "all 0.3s ease",
47
+ width: "100%",
48
+ boxSizing: "border-box",
49
+ // 响应式设计
50
+ "@media (max-width: 600px)": {
51
+ padding: size === Size.SMALL ? "30px 15px" : "40px 20px",
52
+ },
53
+ },
54
+ uploadIcon: {
55
+ fontSize: size === Size.SMALL ? 48 : 64,
56
+ marginBottom: size === Size.SMALL ? 12 : 16,
57
+ },
58
+ uploadTitle: {
59
+ fontSize: size === Size.SMALL ? 16 : 18,
60
+ fontWeight: 600,
61
+ color: "#222",
62
+ marginBottom: size === Size.SMALL ? 8 : 12,
63
+ },
64
+ uploadHint: {
65
+ fontSize: size === Size.SMALL ? 13 : 14,
66
+ color: "#999",
67
+ },
68
+ hiddenInput: {
69
+ display: "none",
70
+ },
71
+ fileList: {
72
+ marginTop: size === Size.SMALL ? 12 : 16,
73
+ },
74
+ fileItem: {
75
+ display: "flex",
76
+ alignItems: "center",
77
+ justifyContent: "space-between",
78
+ padding: size === Size.SMALL ? "8px 12px" : "10px 16px",
79
+ background: "#F5F5F5",
80
+ borderRadius: 6,
81
+ marginBottom: 8,
82
+ },
83
+ fileName: {
84
+ fontSize: size === Size.SMALL ? 13 : 14,
85
+ color: "#222",
86
+ },
87
+ removeButton: {
88
+ background: "#ff4d4f",
89
+ color: "#fff",
90
+ border: "none",
91
+ borderRadius: "50%",
92
+ width: 20,
93
+ height: 20,
94
+ cursor: "pointer",
95
+ fontSize: 12,
96
+ display: "flex",
97
+ alignItems: "center",
98
+ justifyContent: "center",
99
+ },
100
+ fieldSection: {
101
+ marginBottom: size === Size.SMALL ? 20 : 24,
102
+ },
103
+ fieldLabel: {
104
+ display: "block",
105
+ fontSize: size === Size.SMALL ? 14 : 16,
106
+ fontWeight: 500,
107
+ color: "#222",
108
+ marginBottom: size === Size.SMALL ? 8 : 12,
109
+ },
110
+ required: {
111
+ color: "#ff4d4f",
112
+ marginRight: 4,
113
+ },
114
+ inputWrapper: {
115
+ position: "relative",
116
+ },
117
+ input: {
118
+ width: "100%",
119
+ padding: size === Size.SMALL ? "10px 12px" : "12px 16px",
120
+ borderRadius: 6,
121
+ border: "1px solid #D9D9D9",
122
+ outline: "none",
123
+ fontSize: size === Size.SMALL ? 14 : 16,
124
+ boxSizing: "border-box",
125
+ },
126
+ inputError: {
127
+ border: "1px solid #ff4d4f",
128
+ },
129
+ disabled: {
130
+ backgroundColor: "#f5f5f5",
131
+ color: "#999",
132
+ cursor: "not-allowed",
133
+ },
134
+ disabledDark: {
135
+ backgroundColor: "#1A1D23",
136
+ color: "#6B7280",
137
+ cursor: "not-allowed",
138
+ border: "1px solid #374151",
139
+ },
140
+ calendarIcon: {
141
+ position: "absolute",
142
+ right: size === Size.SMALL ? 12 : 16,
143
+ top: "50%",
144
+ transform: "translateY(-50%)",
145
+ fontSize: size === Size.SMALL ? 16 : 18,
146
+ pointerEvents: "none",
147
+ },
148
+ currencySuffix: {
149
+ position: "absolute",
150
+ right: size === Size.SMALL ? 12 : 16,
151
+ top: "50%",
152
+ transform: "translateY(-50%)",
153
+ fontSize: size === Size.SMALL ? 14 : 16,
154
+ color: "#999",
155
+ pointerEvents: "none",
156
+ },
157
+ buttonSection: {
158
+ display: "flex",
159
+ gap: size === Size.SMALL ? 12 : 16,
160
+ marginTop: size === Size.SMALL ? 32 : 40,
161
+ width: "100%",
162
+ boxSizing: "border-box",
163
+ // 响应式设计
164
+ "@media (max-width: 600px)": {
165
+ flexDirection: "column",
166
+ gap: size === Size.SMALL ? 8 : 12,
167
+ },
168
+ },
169
+ cancelButton: {
170
+ flex: 1,
171
+ padding: size === Size.SMALL ? "10px 16px" : "12px 20px",
172
+ borderRadius: 6,
173
+ border: "1px solid #D9D9D9",
174
+ background: "#fff",
175
+ color: "#222",
176
+ fontSize: size === Size.SMALL ? 14 : 16,
177
+ fontWeight: 500,
178
+ cursor: "pointer",
179
+ minWidth: 0,
180
+ boxSizing: "border-box",
181
+ },
182
+ submitButton: {
183
+ flex: 1,
184
+ padding: size === Size.SMALL ? "10px 16px" : "12px 20px",
185
+ borderRadius: 6,
186
+ border: "none",
187
+ background: "#1890ff",
188
+ color: "#fff",
189
+ fontSize: size === Size.SMALL ? 14 : 16,
190
+ fontWeight: 600,
191
+ cursor: "pointer",
192
+ minWidth: 0,
193
+ boxSizing: "border-box",
194
+ },
195
+ error: {
196
+ color: "#ff4d4f",
197
+ fontSize: size === Size.SMALL ? 12 : 13,
198
+ marginTop: 6,
199
+ },
200
+ } as const;
201
+
202
+ return {
203
+ white: {
204
+ ...base,
205
+ },
206
+ dark: {
207
+ ...base,
208
+ container: {
209
+ ...base.container,
210
+ background: "#181A20",
211
+ border: "1px solid #2B2E38",
212
+ },
213
+ backButton: {
214
+ ...base.backButton,
215
+ color: "#00E8C6",
216
+ },
217
+ uploadArea: {
218
+ ...base.uploadArea,
219
+ border: "2px dashed #374151",
220
+ background: "#23262F",
221
+ },
222
+ uploadTitle: {
223
+ ...base.uploadTitle,
224
+ color: "#fff",
225
+ },
226
+ uploadHint: {
227
+ ...base.uploadHint,
228
+ color: "#B5B8BE",
229
+ },
230
+ fileItem: {
231
+ ...base.fileItem,
232
+ background: "#2B2E38",
233
+ },
234
+ fileName: {
235
+ ...base.fileName,
236
+ color: "#fff",
237
+ },
238
+ fieldLabel: {
239
+ ...base.fieldLabel,
240
+ color: "#fff",
241
+ },
242
+ input: {
243
+ ...base.input,
244
+ border: "1px solid #374151",
245
+ background: "#23262F",
246
+ color: "#fff",
247
+ },
248
+ calendarIcon: {
249
+ ...base.calendarIcon,
250
+ color: "#B5B8BE",
251
+ },
252
+ currencySuffix: {
253
+ ...base.currencySuffix,
254
+ color: "#B5B8BE",
255
+ },
256
+ cancelButton: {
257
+ ...base.cancelButton,
258
+ border: "1px solid #374151",
259
+ background: "#23262F",
260
+ color: "#fff",
261
+ },
262
+ submitButton: {
263
+ ...base.submitButton,
264
+ background: "#00E8C6",
265
+ color: "#111",
266
+ },
267
+ },
268
+ } as const;
269
+ }
270
+
271
+ export function getOfflinePaymentTheme() {
272
+ const theme = getInitParams<Theme>("theme");
273
+ const themeConfig = getInitParams<ThemeConfig>("themeConfig");
274
+ const white = theme === Theme.WHITE;
275
+ const themes = createOfflinePaymentThemes();
276
+ const base = white ? themes.white : themes.dark;
277
+
278
+ if (themeConfig) {
279
+ const cfg = white ? themeConfig.white : themeConfig.dark;
280
+ if (cfg?.color) {
281
+ return {
282
+ ...base,
283
+ backButton: { ...base.backButton, color: cfg.color },
284
+ submitButton: { ...base.submitButton, background: cfg.color },
285
+ } as any;
286
+ }
287
+ }
288
+ return base as any;
289
+ }
@@ -0,0 +1,163 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import { t } from "@/local";
3
+ import { getOnlinePaymentTheme } from "./theme";
4
+ import { calcPaymentAmount } from "@/api";
5
+
6
+ interface OnlinePaymentProps {
7
+ onSubmit: (form: {
8
+ amount: string;
9
+ currency: string;
10
+ rechargeChannel: string;
11
+ }) => Promise<void>;
12
+ amount: string;
13
+ currency: string;
14
+ channel: string;
15
+ loading?: boolean;
16
+ error?: string;
17
+ onAmountError?: (error: string) => void;
18
+ }
19
+
20
+ export function OnlinePayment({
21
+ onSubmit,
22
+ amount,
23
+ currency,
24
+ channel,
25
+ loading = false,
26
+ error = "",
27
+ onAmountError,
28
+ }: OnlinePaymentProps) {
29
+ const theme = getOnlinePaymentTheme();
30
+ const isDark = theme.right.background === "#181A20";
31
+
32
+ const [formState, setFormState] = useState({
33
+ amount,
34
+ currency,
35
+ channel,
36
+ loading,
37
+ error,
38
+ amountError: "",
39
+ });
40
+
41
+ const [calc, setCalc] = useState<{
42
+ paymentAmount: number;
43
+ fee: number;
44
+ currency: string;
45
+ } | null>(null);
46
+
47
+ // 同步外部传入的props到内部状态
48
+ useEffect(() => {
49
+ setFormState((s) => ({
50
+ ...s,
51
+ amount,
52
+ currency,
53
+ channel,
54
+ loading,
55
+ error,
56
+ }));
57
+
58
+ // 防抖:300ms 后再请求计算接口
59
+ const timer = setTimeout(() => {
60
+ if (amount && channel && currency) {
61
+ calcPaymentAmount({ channel, amount, currency })
62
+ .then((res) => setCalc(res))
63
+ .catch(() => setCalc(null));
64
+ } else {
65
+ setCalc(null);
66
+ }
67
+ }, 300);
68
+
69
+ return () => clearTimeout(timer);
70
+ }, [amount, currency, channel, loading, error]);
71
+
72
+ const handleSubmit = async (e?: Event) => {
73
+ if (e) e.preventDefault();
74
+ let valid = true;
75
+ if (!amount.trim()) {
76
+ valid = false;
77
+ onAmountError?.(t("请输入充值金额"));
78
+ }
79
+ if (!channel) {
80
+ valid = false;
81
+ setFormState((s) => ({ ...s, error: t("请选择支付平台") }));
82
+ }
83
+ if (!valid) return;
84
+ setFormState((s) => ({ ...s, loading: true, error: "" }));
85
+
86
+ try {
87
+ await onSubmit({
88
+ amount,
89
+ currency,
90
+ rechargeChannel: channel,
91
+ });
92
+ } finally {
93
+ setFormState((s) => ({ ...s, loading: false }));
94
+ }
95
+ };
96
+ return (
97
+ <div style={theme.right}>
98
+ <div style={theme.blockTitle}>
99
+ <span style={theme.titleBar} />
100
+ {t("在线支付")}
101
+ </div>
102
+ <form onSubmit={handleSubmit}>
103
+ <div style={theme.inputWrapper}>
104
+ <input
105
+ type="text"
106
+ placeholder={t("支付金额")}
107
+ value={formState.amount}
108
+ readOnly
109
+ disabled
110
+ style={{
111
+ ...theme.input,
112
+ ...(isDark ? theme.disabledDark : theme.disabled),
113
+ width: "100%",
114
+ }}
115
+ />
116
+ <div style={theme.inputSuffixAbs}>{formState.currency}</div>
117
+ </div>
118
+ <button
119
+ type="submit"
120
+ disabled={formState.loading}
121
+ style={theme.payButton}
122
+ >
123
+ {formState.loading ? t("提交中...") : t("立即支付")}
124
+ </button>
125
+ </form>
126
+
127
+ {calc && (
128
+ <div style={theme.calcBox}>
129
+ <div style={theme.calcLine}>
130
+ {t("预计支付金额约为:")}
131
+ <span style={theme.calcAmount}>{`$${calc.paymentAmount}`}</span>
132
+ {calc.currency}
133
+ </div>
134
+ <div style={theme.calcLine}>
135
+ {t("手续费约为:")}
136
+ <span style={theme.calcAmount}>{`$${calc.fee}`}</span>
137
+ {calc.currency}
138
+ </div>
139
+ <div>{t("注:地区/币种/卡类型可能影响手续费,最终以平台为准。")}</div>
140
+ </div>
141
+ )}
142
+
143
+ <div style={theme.noticeBox}>
144
+ {t("请在转账后提交银行回单,否则可能无法及时核验")}
145
+ </div>
146
+
147
+ <div style={theme.reminderTitle}>{t("温馨提示")}</div>
148
+ <ul style={theme.reminderList}>
149
+ <li>{t("支付金额须与申请金额一致,请勿多付、少付或分批提交。")}</li>
150
+ <li>{t("请妥善保存回单并提供准确的支付时间,否则可能无法核验。")}</li>
151
+ <li>
152
+ {t("工作时间提交4小时内审核,非工作时间将顺延至下一个工作日。")}
153
+ </li>
154
+ <li>{t("如有问题,请联系平台客服。")}</li>
155
+ <li>{t("预付充值将收取平台服务费,具体以官方为准。")}</li>
156
+ </ul>
157
+
158
+ {formState.error && <div style={theme.error}>{formState.error}</div>}
159
+ </div>
160
+ );
161
+ }
162
+
163
+ export default OnlinePayment;
@@ -0,0 +1,179 @@
1
+ import { Size, Theme, type ThemeConfig } from "@/types";
2
+ import { getInitParams } from "@/utils/business";
3
+
4
+ function createOnlinePaymentThemes() {
5
+ const size = getInitParams<Size>("size");
6
+ const base = {
7
+ right: { background: "#fff", borderRadius: 8, padding: 16 },
8
+ blockTitle: {
9
+ fontSize: size === Size.SMALL ? 16 : 18,
10
+ fontWeight: 700,
11
+ marginBottom: size === Size.SMALL ? 12 : 16,
12
+ textAlign: "left",
13
+ display: "flex",
14
+ alignItems: "center",
15
+ gap: 8,
16
+ },
17
+ titleBar: {
18
+ width: 3,
19
+ height: size === Size.SMALL ? 16 : 20,
20
+ background: "#1890ff",
21
+ borderRadius: 2,
22
+ },
23
+ inputWrapper: { position: "relative" },
24
+ input: {
25
+ width: "100%",
26
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
27
+ borderRadius: 6,
28
+ outline: "none",
29
+ border: "1px solid",
30
+ boxSizing: "border-box",
31
+ paddingRight: size === Size.SMALL ? 48 : 64,
32
+ },
33
+ inputError: { border: "1px solid #ff4d4f" },
34
+ disabled: {
35
+ backgroundColor: "#f5f5f5",
36
+ color: "#999",
37
+ cursor: "not-allowed",
38
+ },
39
+ disabledDark: {
40
+ backgroundColor: "#1A1D23",
41
+ color: "#6B7280",
42
+ cursor: "not-allowed",
43
+ border: "1px solid #374151",
44
+ },
45
+ inputSuffixAbs: {
46
+ position: "absolute",
47
+ top: 0,
48
+ right: size === Size.SMALL ? 10 : 12,
49
+ height: "100%",
50
+ display: "flex",
51
+ alignItems: "center",
52
+ color: "#999",
53
+ pointerEvents: "none",
54
+ fontSize: size === Size.SMALL ? 12 : 13,
55
+ },
56
+ payButton: {
57
+ width: "100%",
58
+ marginTop: size === Size.SMALL ? 10 : 14,
59
+ marginBottom: size === Size.SMALL ? 12 : 16,
60
+ background: "#1890ff",
61
+ color: "#fff",
62
+ border: "none",
63
+ borderRadius: 6,
64
+ padding: size === Size.SMALL ? "8px 12px" : "12px 16px",
65
+ fontWeight: 600,
66
+ cursor: "pointer",
67
+ },
68
+ noticeBox: {
69
+ marginTop: size === Size.SMALL ? 8 : 12,
70
+ marginBottom: size === Size.SMALL ? 12 : 16,
71
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
72
+ borderRadius: 6,
73
+ background: "#FFF7E6",
74
+ border: "1px solid #FFE7BA",
75
+ color: "#AD6800",
76
+ fontSize: size === Size.SMALL ? 12 : 13,
77
+ },
78
+ calcBox: {
79
+ marginTop: size === Size.SMALL ? 8 : 12,
80
+ marginBottom: size === Size.SMALL ? 8 : 12,
81
+ color: "#666",
82
+ textAlign: "left",
83
+ lineHeight: 1.6,
84
+ fontSize: size === Size.SMALL ? 12 : 13,
85
+ },
86
+ calcLine: {
87
+ marginBottom: 4,
88
+ },
89
+ calcAmount: {
90
+ color: "#ff4d4f",
91
+ fontWeight: 700,
92
+ margin: "0 4px",
93
+ },
94
+ reminderTitle: { fontWeight: 600, margin: "6px 0", textAlign: "left" },
95
+ reminderList: {
96
+ margin: 0,
97
+ paddingLeft: 18,
98
+ color: "#6b7280",
99
+ lineHeight: 1.6,
100
+ textAlign: "left",
101
+ },
102
+ error: {
103
+ color: "#ff4d4f",
104
+ marginTop: 6,
105
+ fontSize: size === Size.SMALL ? 12 : 13,
106
+ },
107
+ } as const;
108
+
109
+ return {
110
+ white: {
111
+ ...base,
112
+ input: {
113
+ ...base.input,
114
+ border: "1px solid #E5E6EB",
115
+ background: "#fff",
116
+ color: "#222",
117
+ },
118
+ },
119
+ dark: {
120
+ ...base,
121
+ right: {
122
+ background: "#181A20",
123
+ borderRadius: 8,
124
+ padding: 16,
125
+ border: "1px solid #2B2E38",
126
+ },
127
+ titleBar: {
128
+ ...base.titleBar,
129
+ background: "#00E8C6",
130
+ },
131
+ input: {
132
+ ...base.input,
133
+ border: "1px solid #374151",
134
+ background: "#23262F",
135
+ color: "#fff",
136
+ },
137
+ payButton: {
138
+ ...base.payButton,
139
+ background: "#00E8C6",
140
+ color: "#111",
141
+ fontWeight: 700,
142
+ },
143
+ noticeBox: {
144
+ ...base.noticeBox,
145
+ background: "#2A1F0F",
146
+ border: "1px solid #5C3E12",
147
+ color: "#F4E3C1",
148
+ },
149
+ reminderTitle: {
150
+ ...base.reminderTitle,
151
+ color: "#fff",
152
+ },
153
+ reminderList: {
154
+ ...base.reminderList,
155
+ color: "#B5B8BE",
156
+ },
157
+ },
158
+ } as const;
159
+ }
160
+
161
+ export function getOnlinePaymentTheme() {
162
+ const theme = getInitParams<Theme>("theme");
163
+ const themeConfig = getInitParams<ThemeConfig>("themeConfig");
164
+ const white = theme === Theme.WHITE;
165
+ const themes = createOnlinePaymentThemes();
166
+ const base = white ? themes.white : themes.dark;
167
+
168
+ if (themeConfig) {
169
+ const cfg = white ? themeConfig.white : themeConfig.dark;
170
+ if (cfg?.color) {
171
+ return {
172
+ ...base,
173
+ payButton: { ...base.payButton, background: cfg.color },
174
+ titleBar: { ...base.titleBar, background: cfg.color },
175
+ } as any;
176
+ }
177
+ }
178
+ return base as any;
179
+ }