best-unit 1.5.3 → 2.0.2

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.
@@ -139,6 +139,7 @@ declare global {
139
139
  };
140
140
  "best-statistical-balance": any;
141
141
  "best-refresh-button": any;
142
+ "best-payment": any;
142
143
  }
143
144
  }
144
145
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "best-unit",
3
3
  "private": false,
4
- "version": "1.5.3",
4
+ "version": "2.0.2",
5
5
  "type": "module",
6
6
  "main": "dist/best-unit.cjs",
7
7
  "module": "dist/best-unit.js",
package/src/api/index.ts CHANGED
@@ -84,7 +84,9 @@ export const createOfflineRecharge = async (data: any) => {
84
84
  ? String(fundUnitParams.userId)
85
85
  : undefined,
86
86
  transfer_no: data.transferNo,
87
+ transfer_date: data.transferDate,
87
88
  transfer_channel: data.transferChannel,
89
+ payer_name: data.payerName,
88
90
  voucher_urls: data.voucherUrls,
89
91
  };
90
92
  return http().post("/offline/recharge/create", params, {});
@@ -154,5 +156,35 @@ export const getChannelInfo = async (data: { code: string }) => {
154
156
  });
155
157
  };
156
158
 
159
+ // /sdk/offline/channel/map
160
+ export const getOfflineChannelMap = async (data: { currency: string }) => {
161
+ const fundUnitParams = JSON.parse(
162
+ sessionStorage.getItem("fund_unit_params") || "{}"
163
+ );
164
+ return http()
165
+ .get("/offline/channel/map", {
166
+ params: {
167
+ currency: data.currency,
168
+ biz_type: fundUnitParams.bizType,
169
+ },
170
+ })
171
+ .then((res) => {
172
+ return {
173
+ onlineChannel:
174
+ res.data?.online_channels.map((item: any) => ({
175
+ value: item.value,
176
+ label: item.label,
177
+ icon: item.icon,
178
+ })) || [],
179
+ offlineChannel:
180
+ res.data?.offline_channels.map((item: any) => ({
181
+ value: item.value,
182
+ label: item.label,
183
+ icon: item.icon,
184
+ })) || [],
185
+ };
186
+ });
187
+ };
188
+
157
189
  // 示例用法:
158
190
  // getBalance({ merchant_id: '1128', biz_type: 'ad', token: 'xxx' }).then(res => console.log(res));
@@ -0,0 +1,38 @@
1
+ import { useState } from "preact/hooks";
2
+ import { Button } from "@/components/common/button";
3
+ import { PaymentDialog } from "./payment-dialog";
4
+ import register from "preact-custom-element";
5
+ import { createOnlineRecharge } from "@/api";
6
+ import { t } from "@/local";
7
+
8
+ export function BestUnitPayment() {
9
+ const [visible, setVisible] = useState(false);
10
+
11
+ const handleSubmit = async (form: {
12
+ amount: string;
13
+ rechargeChannel: string;
14
+ currency: string;
15
+ }) => {
16
+ const result = await createOnlineRecharge({
17
+ amount: form.amount,
18
+ currency: form.currency,
19
+ rechargeChannel: form.rechargeChannel,
20
+ });
21
+ window.open(result, "_blank");
22
+ };
23
+
24
+ return (
25
+ <div>
26
+ <Button onClick={() => setVisible(true)}>{t("在线支付")}</Button>
27
+ <PaymentDialog
28
+ visible={visible}
29
+ onClose={() => setVisible(false)}
30
+ onSubmit={handleSubmit}
31
+ />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ register(BestUnitPayment, "best-payment", ["theme"], { shadow: false });
37
+
38
+ export default BestUnitPayment;
@@ -0,0 +1,97 @@
1
+ import { t } from "@/local";
2
+ import { getOfflineDetailTheme } from "./theme";
3
+ import { useEffect, useState } from "preact/hooks";
4
+ import { getChannelInfo } from "@/api";
5
+
6
+ interface OfflineDetailProps {
7
+ onConfirm: () => void;
8
+ channelCode: string; // 银行转账渠道编码
9
+ }
10
+
11
+ export function OfflineDetail({ onConfirm, channelCode }: OfflineDetailProps) {
12
+ const theme = getOfflineDetailTheme();
13
+ const [detailLines, setDetailLines] = useState<string[]>([]);
14
+
15
+ useEffect(() => {
16
+ if (!channelCode) return;
17
+ let mounted = true;
18
+ getChannelInfo({ code: channelCode })
19
+ .then((res) => {
20
+ if (!mounted) return;
21
+ if (Array.isArray(res)) {
22
+ setDetailLines(res.map((v) => String(v)));
23
+ return;
24
+ }
25
+ if (res && typeof res === "object") {
26
+ const lines: string[] = [];
27
+ Object.keys(res).forEach((k) => {
28
+ const v = (res as any)[k];
29
+ lines.push(`${k}: ${v}`);
30
+ });
31
+ setDetailLines(lines);
32
+ return;
33
+ }
34
+ setDetailLines(res ? [String(res)] : []);
35
+ })
36
+ .catch(() => setDetailLines([]));
37
+ return () => {
38
+ mounted = false;
39
+ };
40
+ }, [channelCode]);
41
+
42
+ return (
43
+ <div style={theme.container}>
44
+ <div style={theme.header}>
45
+ <span style={theme.titleBar} />
46
+ {t("银行转账")}
47
+ </div>
48
+
49
+ {detailLines.length > 0 && (
50
+ <div style={theme.detailCard}>
51
+ {detailLines.map((line, idx) => {
52
+ const parts = String(line).split(":");
53
+ const label = parts.shift()?.trim() || "";
54
+ const value = parts.join(":").trim();
55
+ return (
56
+ <div
57
+ key={idx}
58
+ style={{
59
+ ...theme.row,
60
+ ...(idx === detailLines.length - 1 ? theme.rowLast : {}),
61
+ ...(idx % 2 === 1 ? theme.rowAlt : {}),
62
+ }}
63
+ >
64
+ <div style={theme.labelMuted}>{label}</div>
65
+ <div style={theme.valueStrong}>{value || "-"}</div>
66
+ </div>
67
+ );
68
+ })}
69
+ </div>
70
+ )}
71
+
72
+ <button type="button" onClick={onConfirm} style={theme.confirmButton}>
73
+ {t("确认转账详情")}
74
+ </button>
75
+
76
+ <div style={theme.noticeBox}>{t("转账后提交银行回单提示")}</div>
77
+
78
+ <div style={theme.reminderTitle}>{t("温馨提示")}</div>
79
+ <ul style={theme.reminderList}>
80
+ <li>{t("支付金额匹配提醒")}</li>
81
+ <li>{t("保存交易回单提醒")}</li>
82
+ <li>{t("查看余额提醒")}</li>
83
+ <li>{t("联系客服提醒")}</li>
84
+ <li>{t("P卡转账延迟提醒")}</li>
85
+ <li>
86
+ {t("平台服务费说明")}{" "}
87
+ <a href="#" style={theme.link}>
88
+ {t("费用政策")}
89
+ </a>{" "}
90
+ {t("详情")}
91
+ </li>
92
+ </ul>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export default OfflineDetail;
@@ -0,0 +1,255 @@
1
+ import { Size, Theme, type ThemeConfig } from "@/types";
2
+ import { getInitParams } from "@/utils/business";
3
+
4
+ function createOfflineDetailThemes() {
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
+ maxWidth: "100%",
17
+ margin: 0,
18
+ padding: size === Size.SMALL ? 12 : 16,
19
+ },
20
+ "@media (min-width: 1021px)": {
21
+ maxWidth: 600,
22
+ margin: "0 auto",
23
+ },
24
+ },
25
+ header: {
26
+ fontSize: size === Size.SMALL ? 16 : 18,
27
+ fontWeight: 700,
28
+ marginBottom: size === Size.SMALL ? 16 : 20,
29
+ textAlign: "left",
30
+ display: "flex",
31
+ alignItems: "center",
32
+ gap: 8,
33
+ },
34
+ titleBar: {
35
+ width: 3,
36
+ height: size === Size.SMALL ? 16 : 20,
37
+ background: "#1890ff",
38
+ borderRadius: 2,
39
+ },
40
+ bankDetails: {
41
+ marginBottom: size === Size.SMALL ? 20 : 24,
42
+ },
43
+ detailCard: {
44
+ marginTop: 12,
45
+ marginBottom: 12,
46
+ border: "1px solid #f0f0f0",
47
+ borderRadius: 8,
48
+ background: "#fafafa", // 统一灰色背景
49
+ overflow: "hidden",
50
+ },
51
+ detailRow: {
52
+ display: "flex",
53
+ justifyContent: "space-between",
54
+ alignItems: "center",
55
+ padding: size === Size.SMALL ? "8px 0" : "10px 0",
56
+ borderBottom: "1px solid #f0f0f0",
57
+ width: "100%",
58
+ boxSizing: "border-box",
59
+ // 响应式设计
60
+ "@media (max-width: 600px)": {
61
+ flexDirection: "column",
62
+ alignItems: "flex-start",
63
+ gap: 4,
64
+ },
65
+ },
66
+ label: {
67
+ fontSize: size === Size.SMALL ? 13 : 14,
68
+ color: "#666",
69
+ fontWeight: 500,
70
+ minWidth: 120,
71
+ // 响应式设计
72
+ "@media (max-width: 600px)": {
73
+ minWidth: "auto",
74
+ marginBottom: 2,
75
+ },
76
+ },
77
+ value: {
78
+ fontSize: size === Size.SMALL ? 13 : 14,
79
+ color: "#222",
80
+ fontWeight: 500,
81
+ textAlign: "right",
82
+ flex: 1,
83
+ // 响应式设计
84
+ "@media (max-width: 600px)": {
85
+ textAlign: "left",
86
+ flex: "none",
87
+ width: "100%",
88
+ },
89
+ },
90
+ valueWithCopy: {
91
+ display: "flex",
92
+ alignItems: "center",
93
+ gap: 8,
94
+ flex: 1,
95
+ justifyContent: "flex-end",
96
+ minWidth: 0,
97
+ // 响应式设计
98
+ "@media (max-width: 600px)": {
99
+ flex: "none",
100
+ width: "100%",
101
+ justifyContent: "flex-start",
102
+ },
103
+ },
104
+ row: {
105
+ display: "flex",
106
+ alignItems: "center",
107
+ padding: "10px 12px",
108
+ borderBottom: "1px solid #ededed", // 行间分隔线
109
+ },
110
+ rowLast: { borderBottom: "none" },
111
+ rowAlt: { background: "#fafafa" },
112
+ labelMuted: {
113
+ fontSize: size === Size.SMALL ? 13 : 14,
114
+ color: "#8c8c8c",
115
+ fontWeight: 500,
116
+ flex: 0.4,
117
+ },
118
+ valueStrong: {
119
+ fontSize: size === Size.SMALL ? 13 : 14,
120
+ color: "#111",
121
+ textAlign: "right",
122
+ flex: 0.6,
123
+ wordBreak: "break-all",
124
+ },
125
+ copyButton: {
126
+ padding: size === Size.SMALL ? "4px 8px" : "6px 12px",
127
+ fontSize: size === Size.SMALL ? 11 : 12,
128
+ background: "#1890ff",
129
+ color: "#fff",
130
+ border: "none",
131
+ borderRadius: 4,
132
+ cursor: "pointer",
133
+ fontWeight: 500,
134
+ flexShrink: 0,
135
+ whiteSpace: "nowrap",
136
+ },
137
+ confirmButton: {
138
+ width: "100%",
139
+ marginBottom: size === Size.SMALL ? 16 : 20,
140
+ background: "#1890ff",
141
+ color: "#fff",
142
+ border: "none",
143
+ borderRadius: 6,
144
+ padding: size === Size.SMALL ? "12px 16px" : "14px 20px",
145
+ fontWeight: 600,
146
+ cursor: "pointer",
147
+ fontSize: size === Size.SMALL ? 14 : 16,
148
+ },
149
+ noticeBox: {
150
+ marginBottom: size === Size.SMALL ? 16 : 20,
151
+ padding: size === Size.SMALL ? "10px 12px" : "12px 16px",
152
+ borderRadius: 6,
153
+ background: "#FFF7E6",
154
+ border: "1px solid #FFE7BA",
155
+ color: "#AD6800",
156
+ fontSize: size === Size.SMALL ? 12 : 13,
157
+ lineHeight: 1.5,
158
+ },
159
+ reminderTitle: {
160
+ fontWeight: 600,
161
+ marginBottom: size === Size.SMALL ? 8 : 12,
162
+ textAlign: "left",
163
+ fontSize: size === Size.SMALL ? 14 : 16,
164
+ },
165
+ reminderList: {
166
+ margin: 0,
167
+ paddingLeft: 18,
168
+ color: "#666",
169
+ lineHeight: 1.6,
170
+ textAlign: "left",
171
+ fontSize: size === Size.SMALL ? 12 : 13,
172
+ },
173
+ link: {
174
+ color: "#1890ff",
175
+ textDecoration: "none",
176
+ fontWeight: 500,
177
+ },
178
+ } as const;
179
+
180
+ return {
181
+ white: {
182
+ ...base,
183
+ },
184
+ dark: {
185
+ ...base,
186
+ container: {
187
+ ...base.container,
188
+ background: "#181A20",
189
+ border: "1px solid #2B2E38",
190
+ },
191
+ titleBar: {
192
+ ...base.titleBar,
193
+ background: "#00E8C6",
194
+ },
195
+ label: {
196
+ ...base.label,
197
+ color: "#B5B8BE",
198
+ },
199
+ value: {
200
+ ...base.value,
201
+ color: "#fff",
202
+ },
203
+ copyButton: {
204
+ ...base.copyButton,
205
+ background: "#00E8C6",
206
+ color: "#111",
207
+ },
208
+ confirmButton: {
209
+ ...base.confirmButton,
210
+ background: "#00E8C6",
211
+ color: "#111",
212
+ },
213
+ noticeBox: {
214
+ ...base.noticeBox,
215
+ background: "#2A1F0F",
216
+ border: "1px solid #5C3E12",
217
+ color: "#F4E3C1",
218
+ },
219
+ reminderTitle: {
220
+ ...base.reminderTitle,
221
+ color: "#fff",
222
+ },
223
+ reminderList: {
224
+ ...base.reminderList,
225
+ color: "#B5B8BE",
226
+ },
227
+ link: {
228
+ ...base.link,
229
+ color: "#00E8C6",
230
+ },
231
+ },
232
+ } as const;
233
+ }
234
+
235
+ export function getOfflineDetailTheme() {
236
+ const theme = getInitParams<Theme>("theme");
237
+ const themeConfig = getInitParams<ThemeConfig>("themeConfig");
238
+ const white = theme === Theme.WHITE;
239
+ const themes = createOfflineDetailThemes();
240
+ const base = white ? themes.white : themes.dark;
241
+
242
+ if (themeConfig) {
243
+ const cfg = white ? themeConfig.white : themeConfig.dark;
244
+ if (cfg?.color) {
245
+ return {
246
+ ...base,
247
+ titleBar: { ...base.titleBar, background: cfg.color },
248
+ copyButton: { ...base.copyButton, background: cfg.color },
249
+ confirmButton: { ...base.confirmButton, background: cfg.color },
250
+ link: { ...base.link, color: cfg.color },
251
+ } as any;
252
+ }
253
+ }
254
+ return base as any;
255
+ }
@@ -0,0 +1,234 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import { t } from "@/local";
3
+ import { getOfflinePaymentTheme } from "./theme";
4
+ import { DatePicker } from "@/components/common/date-picker";
5
+ import { Upload } from "@/components/common/upload";
6
+ import { createOfflineRecharge } from "@/api";
7
+ import { message } from "@/components/common/message";
8
+
9
+ interface OfflinePaymentProps {
10
+ onBack: () => void;
11
+ onCancel: () => void;
12
+ currency?: string;
13
+ amount?: string;
14
+ channel?: string;
15
+ onAmountError?: (error: string) => void;
16
+ }
17
+
18
+ export function OfflinePayment({
19
+ onBack,
20
+ onCancel,
21
+ currency = "USD",
22
+ amount = "",
23
+ channel = "",
24
+ onAmountError,
25
+ }: OfflinePaymentProps) {
26
+ const theme = getOfflinePaymentTheme();
27
+ const isDark = theme.container.background === "#181A20";
28
+
29
+ const [formState, setFormState] = useState({
30
+ urls: [] as string[],
31
+ transferTime: "",
32
+ transferAmount: amount,
33
+ transactionId: "",
34
+ payerName: "",
35
+ filesError: "",
36
+ timeError: "",
37
+ amountError: "",
38
+ transactionIdError: "",
39
+ payerNameError: "",
40
+ });
41
+
42
+ // 同步外部传入的amount到内部状态
43
+ useEffect(() => {
44
+ setFormState((s) => ({ ...s, transferAmount: amount }));
45
+ }, [amount]);
46
+
47
+ // 处理提交
48
+ const handleSubmit = async () => {
49
+ let valid = true;
50
+
51
+ if (formState.urls.length === 0) {
52
+ valid = false;
53
+ setFormState((s) => ({ ...s, filesError: t("请上传银行转账回单") }));
54
+ }
55
+
56
+ if (!formState.transferTime) {
57
+ valid = false;
58
+ setFormState((s) => ({ ...s, timeError: t("请选择转账时间") }));
59
+ }
60
+
61
+ if (!amount.trim()) {
62
+ valid = false;
63
+ onAmountError?.(t("请输入充值金额"));
64
+ }
65
+
66
+ if (!formState.transactionId.trim()) {
67
+ valid = false;
68
+ setFormState((s) => ({
69
+ ...s,
70
+ transactionIdError: t("请输入转账交易ID"),
71
+ }));
72
+ }
73
+
74
+ if (!formState.payerName.trim()) {
75
+ valid = false;
76
+ setFormState((s) => ({
77
+ ...s,
78
+ payerNameError: t("请输入付款人名称"),
79
+ }));
80
+ }
81
+
82
+ if (!valid) return;
83
+
84
+ try {
85
+ await createOfflineRecharge({
86
+ transferDate: formState.transferTime,
87
+ transferNo: formState.transactionId,
88
+ transferChannel: channel,
89
+ voucherUrls: formState.urls,
90
+ payerName: formState.payerName,
91
+ });
92
+ message.success(t("提交成功"));
93
+ onCancel();
94
+ } finally {
95
+ setFormState((s) => ({ ...s, loading: false }));
96
+ }
97
+ };
98
+
99
+ return (
100
+ <div style={theme.container}>
101
+ {/* 返回按钮 */}
102
+ <div style={theme.header}>
103
+ <button type="button" onClick={onBack} style={theme.backButton}>
104
+ <span style={theme.backIcon}>←</span>
105
+ {t("返回账户信息")}
106
+ </button>
107
+ </div>
108
+
109
+ {/* 文件上传区域 */}
110
+ <div style={theme.uploadSection}>
111
+ <Upload
112
+ value={formState.urls}
113
+ onChange={(urls) => {
114
+ setFormState((s) => ({ ...s, urls, filesError: "" }));
115
+ }}
116
+ maxCount={10}
117
+ accept="image/*,.pdf"
118
+ multiple={true}
119
+ />
120
+ {formState.filesError && (
121
+ <div style={theme.error}>{formState.filesError}</div>
122
+ )}
123
+ </div>
124
+
125
+ {/* 转账时间 */}
126
+ <div style={theme.fieldSection}>
127
+ <label style={theme.fieldLabel}>
128
+ <span style={theme.required}>*</span>
129
+ {t("转账时间")}
130
+ </label>
131
+ <DatePicker
132
+ value={formState.transferTime}
133
+ placeholder={t("请选择转账时间")}
134
+ onChange={(value) => {
135
+ setFormState((s) => ({
136
+ ...s,
137
+ transferTime: value,
138
+ timeError: "",
139
+ }));
140
+ }}
141
+ style={{ width: "100%" }}
142
+ />
143
+ {formState.timeError && (
144
+ <div style={theme.error}>{formState.timeError}</div>
145
+ )}
146
+ </div>
147
+
148
+ {/* 转账金额 */}
149
+ <div style={theme.fieldSection}>
150
+ <label style={theme.fieldLabel}>{t("转账金额")}</label>
151
+ <div style={theme.inputWrapper}>
152
+ <input
153
+ type="text"
154
+ placeholder={t("请输入实际转账金额")}
155
+ value={formState.transferAmount}
156
+ readOnly
157
+ disabled
158
+ style={{
159
+ ...theme.input,
160
+ ...(isDark ? theme.disabledDark : theme.disabled),
161
+ }}
162
+ />
163
+ <span style={theme.currencySuffix}>{currency}</span>
164
+ </div>
165
+ </div>
166
+
167
+ {/* 交易ID */}
168
+ <div style={theme.fieldSection}>
169
+ <label style={theme.fieldLabel}>
170
+ <span style={theme.required}>*</span>
171
+ {t("交易ID")}
172
+ </label>
173
+ <input
174
+ type="text"
175
+ placeholder={t("请输入转账交易ID")}
176
+ value={formState.transactionId}
177
+ onChange={(e) => {
178
+ setFormState((s) => ({
179
+ ...s,
180
+ transactionId: (e.target as HTMLInputElement).value,
181
+ transactionIdError: "",
182
+ }));
183
+ }}
184
+ style={{
185
+ ...theme.input,
186
+ ...(formState.transactionIdError ? theme.inputError : {}),
187
+ }}
188
+ />
189
+ {formState.transactionIdError && (
190
+ <div style={theme.error}>{formState.transactionIdError}</div>
191
+ )}
192
+ </div>
193
+
194
+ {/* 付款人名称 */}
195
+ <div style={theme.fieldSection}>
196
+ <label style={theme.fieldLabel}>
197
+ <span style={theme.required}>*</span>
198
+ {t("付款人名称")}
199
+ </label>
200
+ <input
201
+ type="text"
202
+ placeholder={t("请输入付款人名称")}
203
+ value={formState.payerName}
204
+ onChange={(e) => {
205
+ setFormState((s) => ({
206
+ ...s,
207
+ payerName: (e.target as HTMLInputElement).value,
208
+ payerNameError: "",
209
+ }));
210
+ }}
211
+ style={{
212
+ ...theme.input,
213
+ ...(formState.payerNameError ? theme.inputError : {}),
214
+ }}
215
+ />
216
+ {formState.payerNameError && (
217
+ <div style={theme.error}>{formState.payerNameError}</div>
218
+ )}
219
+ </div>
220
+
221
+ {/* 操作按钮 */}
222
+ <div style={theme.buttonSection}>
223
+ <button type="button" onClick={onCancel} style={theme.cancelButton}>
224
+ {t("取消")}
225
+ </button>
226
+ <button type="button" onClick={handleSubmit} style={theme.submitButton}>
227
+ {t("提交审核")}
228
+ </button>
229
+ </div>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ export default OfflinePayment;