misc: binance momentum, telegram, frontend updates

This commit is contained in:
jackyu66git
2026-06-03 09:39:54 +08:00
parent b2ff322ef3
commit 23f3c61c9f
20 changed files with 928 additions and 86 deletions
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Binance Momentum Monitor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/binance-main.jsx"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
import{r as c,j as t,c as B,R as E}from"./client-DYDkQYN6.js";const w="https://www.binance.com/en/futures/";function M(){const[r,S]=c.useState([]),[h,g]=c.useState([]),[C,x]=c.useState("● 连接中..."),[v,u]=c.useState(!1),[i,y]=c.useState("change_abs"),[l,j]=c.useState("desc"),d=c.useRef(null),p=c.useCallback(()=>{const e=new EventSource("/events");d.current=e,e.addEventListener("connected",()=>{x("● 已连接"),u(!0)}),e.onmessage=a=>{try{const n=JSON.parse(a.data);switch(n.event){case"binance_momentum":S(n.data);break;case"binance_alert":g(s=>[n.data,...s].slice(0,200));break}}catch{}},e.onerror=()=>{x("● 已断开 (重连中...)"),u(!1),e.close(),setTimeout(p,3e3)}},[]);c.useEffect(()=>(p(),()=>{d.current&&d.current.close()}),[p]),c.useEffect(()=>{const a=setInterval(async()=>{try{const s=await(await fetch("/api/binance-alerts?limit=50")).json();s.alerts&&s.alerts.length>0&&g(_=>{const $=new Set(_.map(o=>o.timestamp)),N=[..._];for(const o of s.alerts)$.has(o.timestamp)||N.push(o);return N.slice(0,200)})}catch{}},1e4);return()=>clearInterval(a)},[]);const m=e=>{i===e?j(a=>a==="asc"?"desc":"asc"):(y(e),j("desc"))},f=[...r].sort((e,a)=>{let n,s;switch(i){case"coin":return n=e.coin,s=a.coin,l==="asc"?n.localeCompare(s):s.localeCompare(n);case"price":n=e.price||0,s=a.price||0;break;case"change_abs":n=Math.abs(e.change_pct)||0,s=Math.abs(a.change_pct)||0;break;case"change":n=e.change_pct||0,s=a.change_pct||0;break;default:return 0}return l==="asc"?n-s:s-n}),k=r.filter(e=>e.buffer_full).length,b=r.filter(e=>Math.abs(e.change_pct)>=10).length;return t.jsxs("div",{id:"bm-app",children:[t.jsxs("header",{children:[t.jsxs("div",{children:[t.jsx("h1",{children:"BN 1-Min Momentum Monitor"}),t.jsx("span",{className:"header-subtitle",children:"Binance 1分钟涨跌监控 | 阈值 ≥10%"})]}),t.jsxs("div",{className:"header-right",children:[t.jsxs("span",{className:"stat-badge",children:[r.length," coins"]}),t.jsxs("span",{className:"stat-badge",children:[k," ready"]}),b>0&&t.jsxs("span",{className:"stat-badge stat-alert",children:[b," alerting"]}),t.jsx("span",{className:v?"status-online":"status-offline",children:C})]})]}),h.length>0&&t.jsxs("section",{className:"card card-wide alert-section",children:[t.jsxs("h2",{children:["Alert History (",h.length,")"]}),t.jsx("div",{className:"alert-scroll",children:h.slice(0,50).map((e,a)=>{var n,s;return t.jsxs("div",{className:`alert-item alert-${e.direction}`,children:[t.jsx("span",{className:"alert-icon",children:e.direction==="up"?"🟢":"🔴"}),t.jsx("a",{href:`${w}${e.coin}USDT`,target:"_blank",rel:"noreferrer",className:"alert-coin",children:e.coin}),t.jsxs("span",{className:e.direction==="up"?"text-green":"text-red",children:[e.change_pct>=0?"+":"",(n=e.change_pct)==null?void 0:n.toFixed(2),"%"]}),t.jsxs("span",{className:"alert-price",children:["$",(s=e.price)==null?void 0:s.toFixed(4)]}),t.jsx("span",{className:"alert-time text-dim",children:new Date(e.timestamp).toLocaleTimeString()})]},a)})})]}),t.jsxs("section",{className:"card card-wide",children:[t.jsxs("h2",{children:["All Coins (",r.length,")"]}),t.jsx("div",{className:"table-wrap",style:{maxHeight:"calc(100vh - 200px)"},children:t.jsxs("table",{children:[t.jsx("thead",{children:t.jsxs("tr",{children:[t.jsxs("th",{onClick:()=>m("coin"),style:{cursor:"pointer"},children:["Coin ",i==="coin"?l==="asc"?"▲":"▼":""]}),t.jsxs("th",{onClick:()=>m("price"),style:{cursor:"pointer"},children:["Price ",i==="price"?l==="asc"?"▲":"▼":""]}),t.jsxs("th",{onClick:()=>m("change_abs"),style:{cursor:"pointer"},children:["1m Change ",i==="change_abs"?l==="asc"?"▲":"▼":""]}),t.jsx("th",{children:"Bar"})]})}),t.jsxs("tbody",{children:[f.map(e=>{const a=e.buffer_full?e.change_pct>=10?"row-surge-up":e.change_pct<=-10?"row-surge-down":e.change_pct>0?"row-up":e.change_pct<0?"row-down":"":"row-warmup",n=Math.min(Math.abs(e.change_pct)/15*100,100),s=e.change_pct>=10?"bar-green":e.change_pct<=-10?"bar-red":e.change_pct>0?"bar-green-dim":"bar-red-dim";return t.jsxs("tr",{className:a,children:[t.jsxs("td",{children:[t.jsx("a",{href:`${w}${e.coin}USDT`,target:"_blank",rel:"noreferrer",className:"coin-link",children:e.coin}),!e.buffer_full&&t.jsx("span",{className:"warmup-badge",children:"···"})]}),t.jsx("td",{className:"text-right mono",children:e.price?`$${e.price.toFixed(4)}`:"-"}),t.jsx("td",{className:`text-right mono ${e.change_pct>=10?"text-green":e.change_pct<=-10?"text-red":e.change_pct>0?"text-green":e.change_pct<0?"text-red":"text-dim"}`,children:e.buffer_full?`${e.change_pct>=0?"+":""}${e.change_pct.toFixed(2)}%`:"warming..."}),t.jsx("td",{children:t.jsx("div",{className:"bar-track",children:t.jsx("div",{className:`bar-fill ${s}`,style:{width:`${n}%`}})})})]},e.coin)}),f.length===0&&t.jsx("tr",{children:t.jsx("td",{colSpan:4,className:"loading",children:"Waiting for data..."})})]})]})})]})]})}B.createRoot(document.getElementById("root")).render(t.jsx(E.StrictMode,{children:t.jsx(M,{})}));
+1
View File
@@ -0,0 +1 @@
:root{--bg: #0d1117;--card: #161b22;--border: #30363d;--text: #c9d1d9;--text-dim: #8b949e;--accent: #58a6ff;--green: #3fb950;--red: #f85149;--yellow: #d29922}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5;min-height:100vh}#bm-app{max-width:1200px;margin:0 auto;padding:16px}header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--card);border:1px solid var(--border);border-radius:8px;margin-bottom:12px;flex-wrap:wrap;gap:8px}header h1{font-size:18px;font-weight:600}.header-subtitle{font-size:12px;color:var(--text-dim);margin-left:12px}.header-right{display:flex;align-items:center;gap:10px;font-size:13px}.stat-badge{background:#58a6ff1a;color:var(--accent);padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.stat-alert{background:#f8514926;color:var(--red);animation:pulse 2s infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.status-online{color:var(--green)}.status-offline{color:var(--red)}.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:12px}.card h2{font-size:14px;font-weight:600;color:var(--text-dim);margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}.alert-scroll{display:flex;flex-wrap:wrap;gap:6px;max-height:120px;overflow-y:auto}.alert-item{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:4px;font-size:13px;font-variant-numeric:tabular-nums}.alert-up{background:#3fb95014}.alert-down{background:#f8514914}.alert-icon{font-size:14px}.alert-coin{color:var(--accent);text-decoration:none;font-weight:600;min-width:50px}.alert-coin:hover{text-decoration:underline}.alert-price{color:var(--text-dim);min-width:90px}.alert-time{font-size:11px}.table-wrap{overflow-x:auto;overflow-y:auto}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:6px 8px;color:var(--text-dim);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.5px;position:sticky;top:0;background:var(--card);border-bottom:1px solid var(--border);-webkit-user-select:none;user-select:none}th:hover{color:var(--accent)}td{padding:4px 8px;border-bottom:1px solid rgba(48,54,61,.5);white-space:nowrap}tr:hover td{background:#58a6ff0d}.coin-link{color:var(--accent);text-decoration:none;font-weight:600}.coin-link:hover{text-decoration:underline}.warmup-badge{font-size:10px;color:var(--text-dim);margin-left:6px}.row-warmup td{opacity:.45}.row-surge-up td{background:#3fb9501a!important}.row-surge-up:hover td{background:#3fb9502e!important}.row-surge-down td{background:#f851491a!important}.row-surge-down:hover td{background:#f851492e!important}.row-up td{background:#3fb95008}.row-down td{background:#f8514908}.bar-track{width:100px;height:6px;background:#30363d80;border-radius:3px;overflow:hidden}.bar-fill{height:100%;border-radius:3px;transition:width .3s ease}.bar-green{background:var(--green)}.bar-red{background:var(--red)}.bar-green-dim{background:#3fb95080}.bar-red-dim{background:#f8514980}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-dim{color:var(--text-dim)}.text-right{text-align:right}.mono{font-variant-numeric:tabular-nums;font-family:SF Mono,Cascadia Code,monospace}.loading{text-align:center;color:var(--text-dim);padding:20px!important}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#484f58}@media (max-width: 768px){header{flex-direction:column}#bm-app{padding:8px}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Binance Momentum Monitor</title>
<script type="module" crossorigin src="/static/assets/binance-C6HzvyUG.js"></script>
<link rel="modulepreload" crossorigin href="/static/assets/client-DYDkQYN6.js">
<link rel="stylesheet" crossorigin href="/static/assets/binance-DE6-y9x2.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+3 -2
View File
@@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exchange Monitor Dashboard</title>
<script type="module" crossorigin src="/static/assets/index-Czt_9K6K.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-vvNDQq2K.css">
<script type="module" crossorigin src="/static/assets/main-Cg5_dPUr.js"></script>
<link rel="modulepreload" crossorigin href="/static/assets/client-DYDkQYN6.js">
<link rel="stylesheet" crossorigin href="/static/assets/main-vvNDQq2K.css">
</head>
<body>
<div id="root"></div>
+193
View File
@@ -0,0 +1,193 @@
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #c9d1d9;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--yellow: #d29922;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
#bm-app { max-width: 1200px; margin: 0 auto; padding: 16px; }
/* Header */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
}
header h1 { font-size: 18px; font-weight: 600; }
.header-subtitle { font-size: 12px; color: var(--text-dim); margin-left: 12px; }
.header-right { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.stat-badge {
background: rgba(88, 166, 255, 0.1);
color: var(--accent);
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
}
.stat-alert {
background: rgba(248, 81, 73, 0.15);
color: var(--red);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-online { color: var(--green); }
.status-offline { color: var(--red); }
/* Cards */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.card-wide { }
.card h2 {
font-size: 14px;
font-weight: 600;
color: var(--text-dim);
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
/* Alert Section */
.alert-scroll {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 120px;
overflow-y: auto;
}
.alert-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
.alert-up { background: rgba(63, 185, 80, 0.08); }
.alert-down { background: rgba(248, 81, 73, 0.08); }
.alert-icon { font-size: 14px; }
.alert-coin {
color: var(--accent);
text-decoration: none;
font-weight: 600;
min-width: 50px;
}
.alert-coin:hover { text-decoration: underline; }
.alert-price { color: var(--text-dim); min-width: 90px; }
.alert-time { font-size: 11px; }
/* Table */
.table-wrap { overflow-x: auto; overflow-y: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
text-align: left;
padding: 6px 8px;
color: var(--text-dim);
font-weight: 500;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
background: var(--card);
border-bottom: 1px solid var(--border);
user-select: none;
}
th:hover { color: var(--accent); }
td {
padding: 4px 8px;
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
white-space: nowrap;
}
tr:hover td { background: rgba(88, 166, 255, 0.05); }
.coin-link {
color: var(--accent);
text-decoration: none;
font-weight: 600;
}
.coin-link:hover { text-decoration: underline; }
.warmup-badge {
font-size: 10px;
color: var(--text-dim);
margin-left: 6px;
}
/* Row highlights */
.row-warmup td { opacity: 0.45; }
.row-surge-up td { background: rgba(63, 185, 80, 0.1) !important; }
.row-surge-up:hover td { background: rgba(63, 185, 80, 0.18) !important; }
.row-surge-down td { background: rgba(248, 81, 73, 0.1) !important; }
.row-surge-down:hover td { background: rgba(248, 81, 73, 0.18) !important; }
.row-up td { background: rgba(63, 185, 80, 0.03); }
.row-down td { background: rgba(248, 81, 73, 0.03); }
/* Bar */
.bar-track {
width: 100px;
height: 6px;
background: rgba(48, 54, 61, 0.5);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.bar-green { background: var(--green); }
.bar-red { background: var(--red); }
.bar-green-dim { background: rgba(63, 185, 80, 0.5); }
.bar-red-dim { background: rgba(248, 81, 73, 0.5); }
/* Utility */
.text-green { color: var(--green); }
.text-red { color: var(--red); }
.text-dim { color: var(--text-dim); }
.text-right { text-align: right; }
.mono { font-variant-numeric: tabular-nums; font-family: 'SF Mono', 'Cascadia Code', monospace; }
.loading { text-align: center; color: var(--text-dim); padding: 20px !important; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
@media (max-width: 768px) {
header { flex-direction: column; }
#bm-app { padding: 8px; }
}
+215
View File
@@ -0,0 +1,215 @@
import { useState, useEffect, useRef, useCallback } from 'react'
const BINANCE_BASE = 'https://www.binance.com/en/futures/'
function BinanceApp() {
const [coins, setCoins] = useState([])
const [alerts, setAlerts] = useState([])
const [connStatus, setConnStatus] = useState('● 连接中...')
const [connOnline, setConnOnline] = useState(false)
const [sortBy, setSortBy] = useState('change_abs')
const [sortDir, setSortDir] = useState('desc')
const esRef = useRef(null)
// Connect to SSE
const connect = useCallback(() => {
const es = new EventSource('/events')
esRef.current = es
es.addEventListener('connected', () => {
setConnStatus('● 已连接')
setConnOnline(true)
})
// Backend sends events as unnamed SSE messages with JSON {event, data, ts}.
// Use onmessage and dispatch by msg.event field.
es.onmessage = (e) => {
try {
const msg = JSON.parse(e.data)
switch (msg.event) {
case 'binance_momentum':
setCoins(msg.data)
break
case 'binance_alert':
setAlerts(prev => [msg.data, ...prev].slice(0, 200))
break
}
} catch {}
}
es.onerror = () => {
setConnStatus('● 已断开 (重连中...)')
setConnOnline(false)
es.close()
setTimeout(connect, 3000)
}
}, [])
useEffect(() => {
connect()
return () => {
if (esRef.current) esRef.current.close()
}
}, [connect])
// Poll alerts as fallback
useEffect(() => {
const fetchAlerts = async () => {
try {
const res = await fetch('/api/binance-alerts?limit=50')
const data = await res.json()
if (data.alerts && data.alerts.length > 0) {
setAlerts(prev => {
const existing = new Set(prev.map(a => a.timestamp))
const merged = [...prev]
for (const a of data.alerts) {
if (!existing.has(a.timestamp)) {
merged.push(a)
}
}
return merged.slice(0, 200)
})
}
} catch {}
}
const timer = setInterval(fetchAlerts, 10000)
return () => clearInterval(timer)
}, [])
// Sort coins
const handleSort = (col) => {
if (sortBy === col) {
setSortDir(d => d === 'asc' ? 'desc' : 'asc')
} else {
setSortBy(col)
setSortDir('desc')
}
}
const sortedCoins = [...coins].sort((a, b) => {
let va, vb
switch (sortBy) {
case 'coin':
va = a.coin; vb = b.coin
return sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va)
case 'price':
va = a.price || 0; vb = b.price || 0
break
case 'change_abs':
va = Math.abs(a.change_pct) || 0; vb = Math.abs(b.change_pct) || 0
break
case 'change':
va = a.change_pct || 0; vb = b.change_pct || 0
break
default:
return 0
}
return sortDir === 'asc' ? va - vb : vb - va
})
// Stats
const fullCount = coins.filter(c => c.buffer_full).length
const alertingCount = coins.filter(c => Math.abs(c.change_pct) >= 10).length
return (
<div id="bm-app">
{/* Header */}
<header>
<div>
<h1>BN 1-Min Momentum Monitor</h1>
<span className="header-subtitle">Binance 1分钟涨跌监控 | 阈值 &ge;10%</span>
</div>
<div className="header-right">
<span className="stat-badge">{coins.length} coins</span>
<span className="stat-badge">{fullCount} ready</span>
{alertingCount > 0 && <span className="stat-badge stat-alert">{alertingCount} alerting</span>}
<span className={connOnline ? 'status-online' : 'status-offline'}>{connStatus}</span>
</div>
</header>
{/* Alert History */}
{alerts.length > 0 && (
<section className="card card-wide alert-section">
<h2>Alert History ({alerts.length})</h2>
<div className="alert-scroll">
{alerts.slice(0, 50).map((a, i) => (
<div key={i} className={`alert-item alert-${a.direction}`}>
<span className="alert-icon">{a.direction === 'up' ? '🟢' : '🔴'}</span>
<a href={`${BINANCE_BASE}${a.coin}USDT`} target="_blank" rel="noreferrer" className="alert-coin">{a.coin}</a>
<span className={a.direction === 'up' ? 'text-green' : 'text-red'}>
{a.change_pct >= 0 ? '+' : ''}{a.change_pct?.toFixed(2)}%
</span>
<span className="alert-price">${a.price?.toFixed(4)}</span>
<span className="alert-time text-dim">{new Date(a.timestamp).toLocaleTimeString()}</span>
</div>
))}
</div>
</section>
)}
{/* Coins Table */}
<section className="card card-wide">
<h2>All Coins ({coins.length})</h2>
<div className="table-wrap" style={{ maxHeight: 'calc(100vh - 200px)' }}>
<table>
<thead>
<tr>
<th onClick={() => handleSort('coin')} style={{ cursor: 'pointer' }}>
Coin {sortBy === 'coin' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
</th>
<th onClick={() => handleSort('price')} style={{ cursor: 'pointer' }}>
Price {sortBy === 'price' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
</th>
<th onClick={() => handleSort('change_abs')} style={{ cursor: 'pointer' }}>
1m Change {sortBy === 'change_abs' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
</th>
<th>Bar</th>
</tr>
</thead>
<tbody>
{sortedCoins.map(c => {
const cls = !c.buffer_full ? 'row-warmup'
: c.change_pct >= 10 ? 'row-surge-up'
: c.change_pct <= -10 ? 'row-surge-down'
: c.change_pct > 0 ? 'row-up'
: c.change_pct < 0 ? 'row-down'
: ''
const barPct = Math.min(Math.abs(c.change_pct) / 15 * 100, 100)
const barCls = c.change_pct >= 10 ? 'bar-green'
: c.change_pct <= -10 ? 'bar-red'
: c.change_pct > 0 ? 'bar-green-dim'
: 'bar-red-dim'
return (
<tr key={c.coin} className={cls}>
<td>
<a href={`${BINANCE_BASE}${c.coin}USDT`} target="_blank" rel="noreferrer" className="coin-link">
{c.coin}
</a>
{!c.buffer_full && <span className="warmup-badge">···</span>}
</td>
<td className="text-right mono">
{c.price ? `$${c.price.toFixed(4)}` : '-'}
</td>
<td className={`text-right mono ${c.change_pct >= 10 ? 'text-green' : c.change_pct <= -10 ? 'text-red' : c.change_pct > 0 ? 'text-green' : c.change_pct < 0 ? 'text-red' : 'text-dim'}`}>
{c.buffer_full ? `${c.change_pct >= 0 ? '+' : ''}${c.change_pct.toFixed(2)}%` : 'warming...'}
</td>
<td>
<div className="bar-track">
<div className={`bar-fill ${barCls}`} style={{ width: `${barPct}%` }} />
</div>
</td>
</tr>
)
})}
{sortedCoins.length === 0 && (
<tr><td colSpan={4} className="loading">Waiting for data...</td></tr>
)}
</tbody>
</table>
</div>
</section>
</div>
)
}
export default BinanceApp
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import BinanceApp from './BinanceApp'
import './BinanceApp.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BinanceApp />
</React.StrictMode>,
)
+7
View File
@@ -1,5 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
@@ -13,5 +14,11 @@ export default defineConfig({
},
build: {
outDir: 'dist',
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
binance: resolve(__dirname, 'binance.html'),
},
},
},
})