// ==UserScript== // @name 币安盯盘助手 // @namespace http://tampermonkey.net/ // @version 1.8.6 // @description 使用WebSocket实时显示多个币安交易对价格及24小时涨跌幅。 // @author Grok // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @licens MII // @downloadURL none // ==/UserScript== (() => { 'use strict'; const CONFIG = { WS_URL: 'wss://stream.binance.com:9443/ws', RECONNECT_DELAY: 2000, INITIAL_TOP: 15, INITIAL_RIGHT: 15, WIDTH: 200, PADDING: 12, DEFAULT_PAIRS: ['ethusdt'], MAX_PAIRS: 10, STORAGE_KEY: 'binance_tracker_pairs' }; if (document.getElementById('price-tracker')) return; const utils = { savePairs: pairs => GM_setValue(CONFIG.STORAGE_KEY, JSON.stringify(pairs)), loadPairs: () => JSON.parse(GM_getValue(CONFIG.STORAGE_KEY) || JSON.stringify(CONFIG.DEFAULT_PAIRS)) }; const ui = { wsSubscriptions: new Map(), isDragging: false, offsetX: 0, offsetY: 0, init() { this.container = Object.assign(document.createElement('div'), { id: 'price-tracker', style: `top:${CONFIG.INITIAL_TOP}px;right:${CONFIG.INITIAL_RIGHT}px;min-width:${CONFIG.WIDTH}px;padding:${CONFIG.PADDING}px` }); this.container.innerHTML = `
币安助手 ⚙️
`; this.settingsBtn = this.container.querySelector('.settings-btn'); this.settingsPanel = this.container.querySelector('.settings-panel'); this.pairsContainer = this.container.querySelector('.pairs-container'); this.pairInput = this.settingsPanel.querySelector('.pair-input'); this.addPairBtn = this.settingsPanel.querySelector('.add-pair-btn'); document.body.appendChild(this.container); this.injectStyles(); this.addEventListeners(); utils.loadPairs().forEach(pair => this.addPair(pair)); }, injectStyles() { document.head.appendChild(Object.assign(document.createElement('style'), { textContent: ` #price-tracker{position:fixed;background:linear-gradient(145deg,#2d3236,#202529);border:1px solid rgba(255,255,255,0.05);border-radius:14px;box-shadow:0 10px 28px rgba(0,0,0,0.4),0 1px 4px rgba(0,0,0,0.3);z-index:9999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;cursor:grab;user-select:none;overflow:hidden;backdrop-filter:blur(10px);animation:fadeIn 0.3s ease-out} #price-tracker:hover{box-shadow:0 14px 36px rgba(0,0,0,0.45),0 2px 6px rgba(0,0,0,0.35);transition:box-shadow 0.2s ease} .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px} .title{color:#f0b90b;font-size:20px;font-weight:600;letter-spacing:0.5px;text-shadow:0 1px 1px rgba(0,0,0,0.2)} .settings-btn{font-size:px;color:#f0b90b;cursor:pointer;padding:4px;border-radius:50%;background:rgba(255,255,255,0.06);transition:all 0.25s cubic-bezier(0.4,0,0.2,1);box-shadow:0 1px 3px rgba(0,0,0,0.2)} .settings-btn:hover{transform:rotate(90deg) scale(1.2);background:rgba(255,255,255,0.14);box-shadow:0 2px 5px rgba(0,0,0,0.25)} .pairs-container{max-height:240px;overflow:hidden} .pair-item{margin-bottom:12px;padding:8px;border-radius:10px;background:rgba(255,255,255,0.05);position:relative;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);animation:slideIn 0.3s ease-out;box-shadow:0 1px 3px rgba(0,0,0,0.15)} .pair-item:hover{background:rgba(255,255,255,0.08);box-shadow:0 3px 6px rgba(0,0,0,0.2)} .remove-btn{position:absolute;top:6px;right:6px;font-size:12px;color:#ff6666;cursor:pointer;opacity:0;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);background:rgba(255,102,102,0.12);border-radius:50%;width:18px;height:18px;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 1px rgba(0,0,0,0.2)} .pair-item:hover .remove-btn{opacity:1;transform:rotate(90deg)} .remove-btn:hover{background:rgba(255,102,102,0.25);color:#ff8888;transform:rotate(180deg) scale(1.15)} .price-header{display:flex;align-items:center;gap:8px;margin-bottom:6px} .symbol{color:#f0b90b;font-size:14px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;text-shadow:0 1px 1px rgba(0,0,0,0.2)} .price-container{display:flex;align-items:center;justify-content:space-between;gap:12px} .price-value{color:#fff;font-weight:700;font-size:20px;letter-spacing:-0.5px;text-shadow:0 1px 3px rgba(0,0,0,0.3);transition:color 0.4s ease} .price-value.updated-up{animation:pricePulseUp 0.4s ease-in-out} .price-value.updated-down{animation:pricePulseDown 0.4s ease-in-out} .change-24h{font-weight:600;font-size:13px;padding:4px 8px;border-radius:8px;background:rgba(255,255,255,0.08);box-shadow:inset 0 1px 2px rgba(0,0,0,0.15),0 1px 1px rgba(0,0,0,0.1);transition:all 0.25s cubic-bezier(0.4,0,0.2,1)} .change-24h:hover{transform:scale(1.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.2),0 2px 3px rgba(0,0,0,0.15)} .positive{color:#00ee00}.negative{color:#ff6666} .price-time{color:#c0c8d0;font-size:11px;margin-top:6px;opacity:0.9;letter-spacing:0.3px;text-shadow:0 1px 1px rgba(0,0,0,0.15)} .status-dot{background:#00ee00;border-radius:50%;width:8px;height:8px;box-shadow:0 0 4px rgba(0,238,0,0.7);animation:dotPulse 2s infinite cubic-bezier(0.4,0,0.6,1);flex-shrink:0} .settings-panel{position:absolute;top:36px;right:${CONFIG.PADDING}px;background:#2d3236;border:1px solid rgba(255,255,255,0.07);border-radius:12px;padding:10px;box-shadow:0 6px 16px rgba(0,0,0,0.4);z-index:10000;backdrop-filter:blur(8px);opacity:0;transform:translateY(-8px);transition:all 0.3s cubic-bezier(0.4,0,0.2,1)} .settings-panel.active{opacity:1;transform:translateY(0)} .pair-input{padding:6px 10px;border:none;border-radius:8px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;margin-right:8px;outline:none;width:100px;transition:all 0.2s ease;box-shadow:0 1px 1px rgba(0,0,0,0.2)} .pair-input:focus{background:rgba(255,255,255,0.16);transform:scale(1.03);box-shadow:0 2px 3px rgba(0,0,0,0.25)} .pair-input::placeholder{color:#c0c8d0;opacity:0.75} .add-pair-btn{padding:6px 12px;border:none;border-radius:8px;background:#f0b90b;color:#202529;font-weight:600;font-size:13px;cursor:pointer;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);box-shadow:0 1px 4px rgba(240,185,11,0.35)} .add-pair-btn:hover{background:#ffc107;transform:translateY(-2px) scale(1.06);box-shadow:0 3px 8px rgba(240,185,11,0.5)} @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} @keyframes slideIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}} @keyframes slideOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-8px)}} @keyframes pricePulseUp{0%{transform:scale(1);color:#fff}50%{transform:scale(1.06);color:#00ee00}100%{transform:scale(1);color:#fff}} @keyframes pricePulseDown{0%{transform:scale(1);color:#fff}50%{transform:scale(1.06);color:#ff6666}100%{transform:scale(1);color:#fff}} @keyframes dotPulse{0%{transform:scale(1);opacity:1;box-shadow:0 0 4px rgba(0,238,0,0.7)}50%{transform:scale(1.25);opacity:0.7;box-shadow:0 0 8px rgba(0,238,0,0.9)}100%{transform:scale(1);opacity:1;box-shadow:0 0 4px rgba(0,238,0,0.7)}} ` })); }, addEventListeners() { const handleDragStart = (e) => { if (!this.settingsPanel.contains(e.target) && e.button === 0) { this.isDragging = true; const rect = this.container.getBoundingClientRect(); this.offsetX = e.clientX - rect.left; this.offsetY = e.clientY - rect.top; this.container.style.cursor = 'grabbing'; e.preventDefault(); } }; const handleDragMove = (e) => { if (this.isDragging) { const newLeft = Math.max(0, Math.min(e.clientX - this.offsetX, window.innerWidth - this.container.offsetWidth)); const newTop = Math.max(0, Math.min(e.clientY - this.offsetY, window.innerHeight - this.container.offsetHeight)); requestAnimationFrame(() => { this.container.style.left = `${newLeft}px`; this.container.style.top = `${newTop}px`; this.container.style.right = 'auto'; }); } }; const handleDragEnd = () => { if (this.isDragging) { this.isDragging = false; this.container.style.cursor = 'grab'; } }; this.container.addEventListener('mousedown', handleDragStart); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); this.settingsBtn.addEventListener('click', (e) => (e.stopPropagation(), this.toggleSettings())); this.addPairBtn.addEventListener('click', () => this.handleAddPair()); this.pairInput.addEventListener('keypress', (e) => e.key === 'Enter' && this.handleAddPair()); this.container.addEventListener('click', (e) => this.settingsPanel.contains(e.target) && e.stopPropagation()); }, addPair(pair) { if (this.wsSubscriptions.size >= CONFIG.MAX_PAIRS) return alert(`最多支持 ${CONFIG.MAX_PAIRS} 个交易对`); if (this.wsSubscriptions.has(pair)) return; const pairDiv = Object.assign(document.createElement('div'), { className: 'pair-item' }); pairDiv.innerHTML = `
${pair.toUpperCase()}
$--.----.--%
更新时间: --:--:--
`; pairDiv.querySelector('.remove-btn').addEventListener('click', (e) => (e.stopPropagation(), this.removePair(pair))); this.pairsContainer.appendChild(pairDiv); const elements = { priceValue: pairDiv.querySelector('.price-value'), change24h: pairDiv.querySelector('.change-24h'), priceTime: pairDiv.querySelector('.price-time'), statusDot: pairDiv.querySelector('.status-dot'), element: pairDiv, lastPrice: null }; this.wsSubscriptions.set(pair, elements); priceTracker.subscribePair(); this.savePairs(); }, removePair(pair) { const sub = this.wsSubscriptions.get(pair); if (sub) { sub.element.style.animation = 'slideOut 0.3s ease-in forwards'; sub.element.addEventListener('animationend', () => { sub.element.remove(); this.wsSubscriptions.delete(pair); priceTracker.subscribePair(); this.savePairs(); }, { once: true }); } }, handleAddPair() { const pair = this.pairInput.value.trim().toLowerCase(); if (pair && !this.wsSubscriptions.has(pair)) { this.addPair(pair); this.pairInput.value = ''; } }, savePairs: () => utils.savePairs([...ui.wsSubscriptions.keys()]), updateDisplay(pair, price, change, time, status) { const sub = this.wsSubscriptions.get(pair); if (!sub) return; const currentPrice = parseFloat(price.replace('$', '')); if (sub.lastPrice !== null && currentPrice !== sub.lastPrice) { sub.priceValue.textContent = price; sub.priceValue.classList.remove('updated-up', 'updated-down'); sub.priceValue.classList.add(currentPrice > sub.lastPrice ? 'updated-up' : 'updated-down'); sub.priceValue.addEventListener('animationend', () => sub.priceValue.classList.remove('updated-up', 'updated-down'), { once: true }); } else { sub.priceValue.textContent = price; } sub.lastPrice = currentPrice; sub.change24h.textContent = change; sub.change24h.classList.remove('positive', 'negative'); if (change !== '--.--%') sub.change24h.classList.add(parseFloat(change) >= 0 ? 'positive' : 'negative'); sub.priceTime.textContent = `更新时间: ${time}`; sub.statusDot.style.background = status === 'success' ? '#00ee00' : '#ff6666'; sub.statusDot.style.boxShadow = status === 'success' ? '0 0 4px rgba(0,238,0,0.7)' : '0 0 4px rgba(255,102,102,0.7)'; }, toggleSettings() { const isVisible = this.settingsPanel.style.display !== 'none'; this.settingsPanel.style.display = isVisible ? 'none' : 'block'; this.settingsPanel.classList.toggle('active', !isVisible); } }; const priceTracker = { ws: null, reconnectTimeout: null, connect() { if (this.ws) this.ws.close(); const pairs = [...ui.wsSubscriptions.keys()]; if (!pairs.length) return; this.ws = new WebSocket(`${CONFIG.WS_URL}/${pairs.join('@ticker/')}@ticker`); this.ws.onopen = () => ui.wsSubscriptions.forEach((_, pair) => ui.updateDisplay(pair, '$--.--', '--.--%', '已连接', 'success')); this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); ui.updateDisplay(data.s.toLowerCase(), `$${parseFloat(data.c).toFixed(2)}`, `${parseFloat(data.P).toFixed(2)}%`, new Date().toLocaleTimeString(), 'success'); } catch (e) { console.error('WebSocket data error:', e); } }; this.ws.onerror = () => { ui.wsSubscriptions.forEach((_, pair) => ui.updateDisplay(pair, '错误', '--.--%', '连接失败', 'error')); this.reconnect(); }; this.ws.onclose = () => { ui.wsSubscriptions.forEach((_, pair) => ui.updateDisplay(pair, '断开', '--.--%', '等待重连', 'error')); this.reconnect(); }; }, subscribePair: () => priceTracker.connect(), reconnect() { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = setTimeout(() => !this.ws || this.ws.readyState !== WebSocket.OPEN && this.connect(), CONFIG.RECONNECT_DELAY); }, start: () => ui.init(), stop: () => { if (this.ws) this.ws.close(); clearTimeout(this.reconnectTimeout); } }; priceTracker.start(); window.addEventListener('unload', priceTracker.stop); })();