best-unit 0.0.13 → 0.0.14

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,156 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import HoverPopover from "../../common/HoverPopover";
3
+ import { getBalance } from "../../../api";
4
+ import register from "preact-custom-element";
5
+
6
+ // 默认数据,用于加载时显示
7
+ const defaultBalanceData = {
8
+ available: 0,
9
+ currency: "USD",
10
+ symbol: "$",
11
+ details: [
12
+ { label: "真实金额", value: 0, color: "#15b36b", dot: "#15b36b" },
13
+ { label: "冻结金额", value: 0, color: "#f59e0b", dot: "#f59e0b" },
14
+ { label: "总可用", value: 0, color: "#155EEF", dot: "#15b36b" },
15
+ ],
16
+ };
17
+
18
+ function formatNumber(num: number) {
19
+ return num.toLocaleString("en-US", {
20
+ minimumFractionDigits: 2,
21
+ maximumFractionDigits: 2,
22
+ });
23
+ }
24
+
25
+ function StatisticalBalance() {
26
+ const [balanceData, setBalanceData] = useState(defaultBalanceData);
27
+
28
+ useEffect(() => {
29
+ const fetchBalance = async () => {
30
+ try {
31
+ const balance = await getBalance();
32
+
33
+ // 根据 API 返回的数据构建 balanceData
34
+ const newBalanceData = {
35
+ available: balance.availableAmount,
36
+ currency: "USD", // 可以根据实际 API 返回调整
37
+ symbol: "$",
38
+ details: [
39
+ {
40
+ label: "真实金额",
41
+ value: balance.totalAmount,
42
+ color: "#15b36b",
43
+ dot: "#15b36b",
44
+ },
45
+ {
46
+ label: "冻结金额",
47
+ value: balance.frozenAmount,
48
+ color: "#f59e0b",
49
+ dot: "#f59e0b",
50
+ },
51
+ {
52
+ label: "总可用",
53
+ value: balance.availableAmount,
54
+ color: "#155EEF",
55
+ dot: "#15b36b",
56
+ },
57
+ ],
58
+ };
59
+
60
+ setBalanceData(newBalanceData);
61
+ } catch (err) {
62
+ console.error("获取余额失败:", err);
63
+ // 获取失败时保持默认的 $0 USD 显示
64
+ }
65
+ };
66
+
67
+ fetchBalance();
68
+ }, []);
69
+
70
+ return (
71
+ <HoverPopover
72
+ popover={
73
+ <>
74
+ <div
75
+ style={{
76
+ fontSize: 16,
77
+ fontWeight: 600,
78
+ color: "#222",
79
+ marginBottom: 16,
80
+ textAlign: "center",
81
+ }}
82
+ >
83
+ 余额详情
84
+ </div>
85
+ {balanceData.details.map((item) => (
86
+ <div
87
+ key={item.label}
88
+ style={{
89
+ display: "flex",
90
+ justifyContent: "space-between",
91
+ alignItems: "center",
92
+ padding: "8px 0",
93
+ borderBottom: "1px solid #e5e7eb",
94
+ fontSize: 15,
95
+ }}
96
+ >
97
+ <span
98
+ style={{
99
+ display: "flex",
100
+ alignItems: "center",
101
+ color: "#6b7280",
102
+ fontWeight: 500,
103
+ }}
104
+ >
105
+ <span
106
+ style={{
107
+ display: "inline-block",
108
+ width: 8,
109
+ height: 8,
110
+ borderRadius: "50%",
111
+ background: item.dot,
112
+ marginRight: 8,
113
+ }}
114
+ />
115
+ {item.label}
116
+ </span>
117
+ <span
118
+ style={{ color: item.color, fontWeight: 600, fontSize: 15 }}
119
+ >
120
+ {balanceData.symbol}
121
+ {formatNumber(item.value)}
122
+ </span>
123
+ </div>
124
+ ))}
125
+ </>
126
+ }
127
+ popoverPosition="rightTop"
128
+ >
129
+ <div
130
+ style={{
131
+ fontSize: 24,
132
+ fontWeight: 800,
133
+ color: "#111827",
134
+ display: "inline-block",
135
+ }}
136
+ >
137
+ {balanceData.symbol}
138
+ {formatNumber(balanceData.available)}
139
+ <span
140
+ style={{
141
+ fontSize: 18,
142
+ color: "#6b7280",
143
+ marginLeft: 8,
144
+ fontWeight: 600,
145
+ }}
146
+ >
147
+ {balanceData.currency}
148
+ </span>
149
+ </div>
150
+ </HoverPopover>
151
+ );
152
+ }
153
+
154
+ register(StatisticalBalance, "best-statistical-balance");
155
+
156
+ export default StatisticalBalance;
@@ -0,0 +1,163 @@
1
+ import { useState, useRef } from "preact/hooks";
2
+ import type { FunctionalComponent, JSX } from "preact";
3
+
4
+ interface HoverPopoverProps {
5
+ popover: JSX.Element;
6
+ children: JSX.Element;
7
+ popoverWidth?: number;
8
+ popoverMinWidth?: number;
9
+ offsetY?: number; // 弹层与目标元素的垂直间距
10
+ offsetX?: number; // 弹层与目标元素的水平间距
11
+ popoverPosition?: "top" | "bottom" | "leftTop" | "rightTop";
12
+ }
13
+
14
+ /**
15
+ * 通用 HoverPopover 组件,支持上下、左上、右上浮层、箭头、位置自适应
16
+ */
17
+ const HoverPopover: FunctionalComponent<HoverPopoverProps> = ({
18
+ popover,
19
+ children,
20
+ popoverWidth = 300,
21
+ popoverMinWidth = 200,
22
+ offsetY = 16,
23
+ offsetX = 16,
24
+ popoverPosition = "top",
25
+ }) => {
26
+ const [show, setShow] = useState(false);
27
+ const [position, setPosition] = useState<
28
+ "top" | "bottom" | "leftTop" | "rightTop"
29
+ >(popoverPosition);
30
+ const ref = useRef<HTMLDivElement>(null);
31
+
32
+ const handleMouseEnter = () => {
33
+ if (popoverPosition === "top" || popoverPosition === "bottom") {
34
+ if (ref.current) {
35
+ const rect = ref.current.getBoundingClientRect();
36
+ if (popoverPosition === "top" && rect.top < 100) {
37
+ setPosition("bottom");
38
+ } else if (
39
+ popoverPosition === "bottom" &&
40
+ window.innerHeight - rect.bottom < 100
41
+ ) {
42
+ setPosition("top");
43
+ } else {
44
+ setPosition(popoverPosition);
45
+ }
46
+ } else {
47
+ setPosition(popoverPosition);
48
+ }
49
+ } else {
50
+ setPosition(popoverPosition);
51
+ }
52
+ setShow(true);
53
+ };
54
+
55
+ // 弹层定位样式
56
+ let popoverStyle: any = {
57
+ position: "absolute",
58
+ zIndex: 10,
59
+ background: "#fff",
60
+ color: "#222",
61
+ borderRadius: 6,
62
+ fontSize: 15,
63
+ minWidth: popoverMinWidth,
64
+ width: popoverWidth,
65
+ padding: "8px 14px",
66
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
67
+ pointerEvents: "auto",
68
+ textAlign: "center",
69
+ border: "none",
70
+ animation: "fadeInUp 0.3s",
71
+ };
72
+ let arrowStyle: any = {
73
+ position: "absolute",
74
+ zIndex: 11,
75
+ width: 0,
76
+ height: 0,
77
+ };
78
+ if (position === "top") {
79
+ popoverStyle = {
80
+ ...popoverStyle,
81
+ left: "50%",
82
+ top: -48,
83
+ transform: "translateX(-50%)",
84
+ };
85
+ arrowStyle = {
86
+ ...arrowStyle,
87
+ left: "50%",
88
+ bottom: -8,
89
+ transform: "translateX(-50%)",
90
+ borderLeft: "8px solid transparent",
91
+ borderRight: "8px solid transparent",
92
+ borderTop: "8px solid #fff",
93
+ };
94
+ } else if (position === "bottom") {
95
+ popoverStyle = {
96
+ ...popoverStyle,
97
+ left: "50%",
98
+ top: "100%",
99
+ marginTop: offsetY,
100
+ transform: "translateX(-50%)",
101
+ };
102
+ arrowStyle = {
103
+ ...arrowStyle,
104
+ left: "50%",
105
+ top: -8,
106
+ transform: "translateX(-50%)",
107
+ borderLeft: "8px solid transparent",
108
+ borderRight: "8px solid transparent",
109
+ borderBottom: "8px solid #fff",
110
+ };
111
+ } else if (position === "leftTop") {
112
+ popoverStyle = {
113
+ ...popoverStyle,
114
+ right: "100%",
115
+ top: 0,
116
+ marginRight: offsetX,
117
+ transform: "none",
118
+ };
119
+ arrowStyle = {
120
+ ...arrowStyle,
121
+ right: -8,
122
+ top: 12,
123
+ borderTop: "8px solid transparent",
124
+ borderBottom: "8px solid transparent",
125
+ borderLeft: "8px solid #fff",
126
+ };
127
+ } else if (position === "rightTop") {
128
+ popoverStyle = {
129
+ ...popoverStyle,
130
+ left: "100%",
131
+ top: 0,
132
+ marginLeft: offsetX,
133
+ transform: "translateY(0)", // 右上角对齐
134
+ };
135
+ arrowStyle = {
136
+ ...arrowStyle,
137
+ left: -8,
138
+ top: 12,
139
+ borderTop: "8px solid transparent",
140
+ borderBottom: "8px solid transparent",
141
+ borderRight: "8px solid #fff",
142
+ };
143
+ }
144
+
145
+ return (
146
+ <div
147
+ ref={ref}
148
+ style={{ position: "relative", display: "inline-block" }}
149
+ onMouseEnter={handleMouseEnter}
150
+ onMouseLeave={() => setShow(false)}
151
+ >
152
+ {children}
153
+ {show && (
154
+ <div style={popoverStyle}>
155
+ {popover}
156
+ <div style={arrowStyle} />
157
+ </div>
158
+ )}
159
+ </div>
160
+ );
161
+ };
162
+
163
+ export default HoverPopover;
@@ -0,0 +1,324 @@
1
+ import type { FunctionalComponent } from "preact";
2
+ import { useState, useEffect } from "preact/hooks";
3
+
4
+ export type MessageType = "success" | "error" | "warning" | "info";
5
+
6
+ interface MessageProps {
7
+ type: MessageType;
8
+ content: string;
9
+ duration?: number;
10
+ onClose?: () => void;
11
+ closable?: boolean;
12
+ }
13
+
14
+ const MessageItem: FunctionalComponent<MessageProps> = ({
15
+ type,
16
+ content,
17
+ duration = 3000,
18
+ onClose,
19
+ closable = false,
20
+ }) => {
21
+ const [visible, setVisible] = useState(true);
22
+
23
+ useEffect(() => {
24
+ if (duration > 0) {
25
+ const timer = setTimeout(() => {
26
+ setVisible(false);
27
+ setTimeout(() => onClose?.(), 300);
28
+ }, duration);
29
+ return () => clearTimeout(timer);
30
+ }
31
+ }, [duration, onClose]);
32
+
33
+ const getIcon = () => {
34
+ switch (type) {
35
+ case "error":
36
+ return (
37
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
38
+ <circle cx="8" cy="8" r="8" fill="#ff4d4f" />
39
+ <path
40
+ d="M10.5 5.5l-5 5m0-5l5 5"
41
+ stroke="white"
42
+ strokeWidth="1.5"
43
+ strokeLinecap="round"
44
+ />
45
+ </svg>
46
+ );
47
+ case "success":
48
+ return (
49
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
50
+ <circle cx="8" cy="8" r="8" fill="#52c41a" />
51
+ <path
52
+ d="M5 8.5l2.2 2.2 3.8-3.4"
53
+ stroke="white"
54
+ strokeWidth="1.8"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ fill="none"
58
+ />
59
+ </svg>
60
+ );
61
+ case "warning":
62
+ return (
63
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
64
+ <circle cx="8" cy="8" r="8" fill="#faad14" />
65
+ <path
66
+ d="M8 4.5v5"
67
+ stroke="white"
68
+ strokeWidth="1.5"
69
+ strokeLinecap="round"
70
+ />
71
+ <circle cx="8" cy="12.5" r="1" fill="white" />
72
+ </svg>
73
+ );
74
+ case "info":
75
+ return (
76
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
77
+ <circle cx="8" cy="8" r="8" fill="#1677ff" />
78
+ <rect
79
+ x="7.25"
80
+ y="7"
81
+ width="1.5"
82
+ height="6"
83
+ rx="0.75"
84
+ fill="white"
85
+ />
86
+ <rect
87
+ x="7.25"
88
+ y="5"
89
+ width="1.5"
90
+ height="1.5"
91
+ rx="0.75"
92
+ fill="white"
93
+ />
94
+ </svg>
95
+ );
96
+ default:
97
+ return null;
98
+ }
99
+ };
100
+
101
+ const getBackgroundColor = () => {
102
+ return "#fff"; // 统一使用白色背景
103
+ };
104
+
105
+ const getBorderColor = () => {
106
+ return "#d9d9d9"; // 统一使用灰色边框
107
+ };
108
+
109
+ return (
110
+ <div
111
+ style={{
112
+ position: "fixed",
113
+ top: "20px",
114
+ left: "50%",
115
+ zIndex: 9999,
116
+ background: getBackgroundColor(),
117
+ border: `1px solid ${getBorderColor()}`,
118
+ borderRadius: "12px", // 更圆润的圆角
119
+ padding: "8px 16px", // 减小上下padding
120
+ display: "flex",
121
+ alignItems: "center",
122
+ gap: "8px",
123
+ boxShadow: "0 6px 16px rgba(0, 0, 0, 0.12)", // 增强阴影效果
124
+ minWidth: content.length < 20 ? "auto" : "300px", // 短消息自适应宽度
125
+ maxWidth: "500px",
126
+ opacity: visible ? 1 : 0,
127
+ transform: visible
128
+ ? "translateX(-50%) translateY(0)"
129
+ : "translateX(-50%) translateY(-20px)",
130
+ transition: "all 0.3s ease",
131
+ }}
132
+ >
133
+ <div style={{ flexShrink: 0 }}>{getIcon()}</div>
134
+ <div
135
+ style={{
136
+ color: "#222",
137
+ fontSize: "14px",
138
+ lineHeight: "1.5",
139
+ flex: 1,
140
+ }}
141
+ >
142
+ {content}
143
+ </div>
144
+ {closable && (
145
+ <button
146
+ onClick={() => {
147
+ setVisible(false);
148
+ setTimeout(() => onClose?.(), 300);
149
+ }}
150
+ style={{
151
+ background: "none",
152
+ border: "none",
153
+ cursor: "pointer",
154
+ padding: "4px",
155
+ color: "#999",
156
+ fontSize: "12px",
157
+ flexShrink: 0,
158
+ }}
159
+ >
160
+
161
+ </button>
162
+ )}
163
+ </div>
164
+ );
165
+ };
166
+
167
+ // 支持对象参数
168
+ interface MessageOptions {
169
+ content: string;
170
+ duration?: number;
171
+ closable?: boolean;
172
+ }
173
+
174
+ type MessageArg = string | MessageOptions;
175
+
176
+ // 简单的消息管理器
177
+ class MessageManager {
178
+ private parseArg(arg: MessageArg): MessageOptions {
179
+ if (typeof arg === "string") {
180
+ return { content: arg };
181
+ }
182
+ return arg;
183
+ }
184
+
185
+ show(type: MessageType, arg: MessageArg) {
186
+ const { content, duration, closable } = this.parseArg(arg);
187
+ // 创建消息元素
188
+ const messageDiv = document.createElement("div");
189
+ messageDiv.style.cssText = `
190
+ position: fixed;
191
+ top: 20px;
192
+ left: 50%;
193
+ transform: translateX(-50%);
194
+ z-index: 9999;
195
+ background: #fff;
196
+ border: 1px solid #fff;
197
+ border-radius: 12px;
198
+ padding: 8px 16px;
199
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
200
+ min-width: ${content.length < 20 ? "auto" : "300px"};
201
+ max-width: 500px;
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 8px;
205
+ font-size: 14px;
206
+ color: #222;
207
+ `;
208
+
209
+ // 添加图标
210
+ if (type === "error") {
211
+ const iconSvg = document.createElementNS(
212
+ "http://www.w3.org/2000/svg",
213
+ "svg"
214
+ );
215
+ iconSvg.setAttribute("width", "16");
216
+ iconSvg.setAttribute("height", "16");
217
+ iconSvg.setAttribute("viewBox", "0 0 16 16");
218
+ iconSvg.innerHTML = `
219
+ <circle cx="8" cy="8" r="8" fill="#ff4d4f"/>
220
+ <path d="M10.5 5.5l-5 5m0-5l5 5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
221
+ `;
222
+ messageDiv.appendChild(iconSvg);
223
+ }
224
+ if (type === "success") {
225
+ const iconSvg = document.createElementNS(
226
+ "http://www.w3.org/2000/svg",
227
+ "svg"
228
+ );
229
+ iconSvg.setAttribute("width", "16");
230
+ iconSvg.setAttribute("height", "16");
231
+ iconSvg.setAttribute("viewBox", "0 0 16 16");
232
+ iconSvg.innerHTML = `
233
+ <circle cx="8" cy="8" r="8" fill="#52c41a"/>
234
+ <path d="M5 8.5l2.2 2.2 3.8-3.4" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
235
+ `;
236
+ messageDiv.appendChild(iconSvg);
237
+ }
238
+ if (type === "warning") {
239
+ const iconSvg = document.createElementNS(
240
+ "http://www.w3.org/2000/svg",
241
+ "svg"
242
+ );
243
+ iconSvg.setAttribute("width", "16");
244
+ iconSvg.setAttribute("height", "16");
245
+ iconSvg.setAttribute("viewBox", "0 0 16 16");
246
+ iconSvg.innerHTML = `
247
+ <circle cx="8" cy="8" r="8" fill="#faad14"/>
248
+ <path d="M8 4.5v5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
249
+ <circle cx="8" cy="12.5" r="1" fill="white"/>
250
+ `;
251
+ messageDiv.appendChild(iconSvg);
252
+ }
253
+ if (type === "info") {
254
+ const iconSvg = document.createElementNS(
255
+ "http://www.w3.org/2000/svg",
256
+ "svg"
257
+ );
258
+ iconSvg.setAttribute("width", "16");
259
+ iconSvg.setAttribute("height", "16");
260
+ iconSvg.setAttribute("viewBox", "0 0 16 16");
261
+ iconSvg.innerHTML = `
262
+ <circle cx="8" cy="8" r="8" fill="#1677ff"/>
263
+ <rect x="7.25" y="7" width="1.5" height="6" rx="0.75" fill="white"/>
264
+ <rect x="7.25" y="5" width="1.5" height="1.5" rx="0.75" fill="white"/>
265
+ `;
266
+ messageDiv.appendChild(iconSvg);
267
+ }
268
+
269
+ // 添加文本
270
+ const textDiv = document.createElement("div");
271
+ textDiv.textContent = content;
272
+ textDiv.style.flex = "1";
273
+ messageDiv.appendChild(textDiv);
274
+
275
+ // 添加关闭按钮(如果启用)
276
+ if (closable) {
277
+ const closeBtn = document.createElement("button");
278
+ closeBtn.textContent = "✕";
279
+ closeBtn.style.cssText = `
280
+ background: none;
281
+ border: none;
282
+ cursor: pointer;
283
+ padding: 4px;
284
+ color: #999;
285
+ font-size: 12px;
286
+ `;
287
+ closeBtn.onclick = () => {
288
+ document.body.removeChild(messageDiv);
289
+ };
290
+ messageDiv.appendChild(closeBtn);
291
+ }
292
+
293
+ // 添加到页面
294
+ document.body.appendChild(messageDiv);
295
+
296
+ // 自动关闭
297
+ if (duration !== 0) {
298
+ setTimeout(() => {
299
+ if (document.body.contains(messageDiv)) {
300
+ document.body.removeChild(messageDiv);
301
+ }
302
+ }, duration || 3000);
303
+ }
304
+ }
305
+
306
+ success(arg: MessageArg) {
307
+ this.show("success", arg);
308
+ }
309
+
310
+ error(arg: MessageArg) {
311
+ this.show("error", arg);
312
+ }
313
+
314
+ warning(arg: MessageArg) {
315
+ this.show("warning", arg);
316
+ }
317
+
318
+ info(arg: MessageArg) {
319
+ this.show("info", arg);
320
+ }
321
+ }
322
+
323
+ export const message = new MessageManager();
324
+ export default MessageItem;