// ==UserScript== // @name TradingView - E...
Erstellt am: 10. März 2025
Erstellt am: 10. März 2025
// ==UserScript==
// @name TradingView - Elegant Multi-Timeframe & Centered Alert
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Elegant inline multi-timeframe display and centered frosted glass alert
// @match https://.tradingview.com/
// @icon https://www.tradingview.com/favicon.ico
// @grant none
// ==/UserScript==
(function() {
'use strict';
text/** * ======================================== * 1. 配置与样式部分 * ======================================== */ // 预加载字体 const fontLinkSF = document.createElement('link'); fontLinkSF.rel = 'stylesheet'; fontLinkSF.href = 'https://fonts.cdnfonts.com/css/sf-pro-display'; document.head.appendChild(fontLinkSF); // 注入全局CSS const style = document.createElement('style'); style.textContent = ` /* 全局变量定义 */ :root { --font-primary: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; --font-secondary: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; --bg-frosted: rgba(255, 255, 255, 0.75); --bg-highlight: rgba(255, 255, 255, 0.95); --color-text: #1a1a1a; --color-text-sub: #555; --color-accent: #0066FF; --color-warning: #FF9800; --color-danger: #FF5252; --color-critical: #F44336; --blur-amount: 15px; --container-radius: 16px; --transition-fast: 0.2s cubic-bezier(0.25, 1, 0.5, 1); --transition-normal: 0.3s cubic-bezier(0.25, 1, 0.5, 1); --shadow-normal: 0 8px 24px rgba(0, 0, 0, 0.1); } /* 动画定义 */ @keyframes fadeIn { 0% { opacity: 0; transform: translateY(-6px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes fadeOut { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-6px); } } @keyframes softGlow { 0% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; } 50% { box-shadow: 0 10px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(200, 200, 200, 0.4) inset; } 100% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; } } @keyframes countdownHighlight { 0% { color: var(--color-text); } 30% { color: var(--color-accent); } 100% { color: var(--color-text); } } @keyframes pulseRing { 0% { width: 100%; height: 100%; opacity: 0.8; } 100% { width: 200%; height: 200%; opacity: 0; } } /* 主容器样式 */ .tv-frosted-header { position: absolute; top: 16px; right: 130px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; align-items: flex-end; pointer-events: none; font-family: var(--font-primary); } .tv-frosted-header .container { display: flex; align-items: center; gap: 12px; pointer-events: auto; background: var(--bg-frosted); backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border-radius: var(--container-radius); padding: 8px 16px; box-shadow: var(--shadow-normal); border: 1px solid rgba(200, 200, 200, 0.5); animation: fadeIn 0.5s ease forwards; } /* 倒计时容器 */ .countdowns-wrapper { display: flex; flex-direction: column; position: relative; } .countdown-box { position: relative; display: flex; align-items: center; gap: 12px; } .countdown-popup-wrapper { position: relative; } .countdown-container { display: flex; align-items: center; gap: 8px; background: var(--bg-highlight); border-radius: 12px; padding: 6px 12px; transition: all 0.3s ease; border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .countdown-label { font-size: 13px; font-weight: 600; color: var(--color-text); font-family: var(--font-secondary); text-transform: uppercase; letter-spacing: 0.05em; cursor: pointer; transition: all 0.2s ease; } .countdown-label:hover { color: var(--color-accent); transform: scale(1.05); } /* 翻页数字动画 */ .countdown-digits { display: flex; align-items: center; gap: 4px; } .cd-digit { position: relative; width: 16px; height: 24px; overflow: hidden; border-radius: 4px; background: rgba(245, 245, 245, 0.2); box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.2), 0 2px 5px rgba(0, 0, 0, 0.08); transform-style: preserve-3d; } .cd-digit::before { content: "0\\A 1\\A 2\\A 3\\A 4\\A 5\\A 6\\A 7\\A 8\\A 9"; white-space: pre; position: absolute; width: 100%; height: 1000%; top: calc(var(--value) * -100%); left: 0; color: #222; font-size: 16px; line-height: 24px; font-weight: 700; text-align: center; transition: top 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .cd-digit::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 50%; background: rgba(245, 245, 245, 0.1); transform-origin: bottom; transform: rotateX(-90deg); transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .cd-digit.changing::after { transform: rotateX(0deg); } .cd-separator { font-size: 16px; color: var(--color-text-sub); line-height: 24px; margin: 0 2px; } /* 美东时间模块 */ .ny-time-box { display: flex; align-items: center; gap: 8px; position: relative; background: var(--bg-highlight); border-radius: 12px; padding: 6px 14px; border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); white-space: nowrap; } .ny-time-container { display: flex; align-items: center; gap: 8px; position: relative; } .current-time { font-family: var(--font-secondary); font-size: 16px; font-weight: 700; color: var(--color-text); letter-spacing: 0.5px; white-space: nowrap; } .market-status-dot { width: 10px; height: 10px; border-radius: 50%; position: relative; transition: all 0.3s ease; cursor: pointer; } .market-status-dot.open { background: #00C853; box-shadow: 0 0 8px rgba(0, 200, 83, 0.4); } .market-status-dot.closed { background: #FF9100; box-shadow: 0 0 8px rgba(255, 145, 0, 0.4); } .market-status-dot::after { content: ''; position: absolute; top: 50%; left: 50%; width: 100%; height: 100%; border-radius: 50%; transform: translate(-50%, -50%); animation: pulseRing 2s infinite ease-out; } .market-status-dot::before { content: attr(data-status); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%) translateY(-8px); font-size: 10px; font-weight: 600; white-space: nowrap; color: var(--color-text); background: var(--bg-highlight); padding: 4px 8px; border-radius: 4px; opacity: 0; visibility: hidden; transition: all 0.2s ease; } .market-status-dot:hover::before { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(-4px); } /* 中央10秒倒计时提示 */ .center-countdown-alert { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 999999; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 18px; padding: 8px 28px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; border: 1px solid rgba(200, 200, 200, 0.5); font-family: var(--font-secondary); animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards; pointer-events: auto; text-align: center; min-width: 180px; transition: all 0.6s cubic-bezier(0.2, 0.8, 0.2, 1.0); } .center-countdown-alert:hover { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(200, 200, 200, 0.4) inset; transform: translateX(-50%) translateY(-2px); } .center-countdown-alert.removing { animation: fadeOut 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards; } .center-countdown-alert .countdown-content { font-size: 20px; font-weight: 600; color: #333; letter-spacing: 0.3px; display: flex; align-items: center; justify-content: center; gap: 8px; } .center-countdown-alert .timeframe { color: var(--color-accent); margin-right: 2px; position: relative; } .center-countdown-alert .seconds { color: #FF5252; font-size: 24px; position: relative; font-variant-numeric: tabular-nums; font-weight: 700; } .center-countdown-alert .seconds::after { content: 's'; font-size: 14px; position: absolute; right: -10px; bottom: 2px; opacity: 0.85; font-weight: 500; } /* 内联多周期显示 */ .inline-timeframes { position: absolute; top: 100%; left: 0; display: flex; flex-direction: column; margin-top: 8px; padding: 8px 16px; border-radius: 12px; background: var(--bg-highlight); border: 1px solid rgba(200, 200, 200, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); animation: fadeIn 0.3s ease forwards; font-family: var(--font-primary); width: auto; transform-origin: top left; z-index: 99997; } .inline-timeframes.removing { animation: fadeOut 0.3s ease forwards; } .inline-timeframes-header { display: flex; justify-content: flex-end; width: 100%; margin-bottom: 4px; } .close-timeframes-button { background: none; border: none; font-size: 16px; font-weight: bold; color: #888; cursor: pointer; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .close-timeframes-button:hover { background: rgba(0, 0, 0, 0.05); color: #555; } .inline-timeframes-content { display: flex; flex-direction: row; align-items: center; gap: 16px; } .timeframe-item { display: flex; align-items: center; gap: 8px; padding: 2px 2px; } .timeframe-label { font-size: 13px; font-weight: 700; color: #888; /* 改为灰色 */ text-transform: uppercase; letter-spacing: 0.05em; } .timeframe-time { font-size: 13px; font-weight: 600; color: #333; /* 改为黑色 */ font-variant-numeric: tabular-nums; letter-spacing: 0.02em; transition: all 0.2s ease; } .timeframe-time.highlight { animation: countdownHighlight 2s ease; } /* 设置面板 */ .countdown-settings { position: absolute; top: 100%; right: 0; margin-top: 8px; width: 280px; background: var(--bg-frosted); backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border-radius: var(--container-radius); border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: var(--shadow-normal); padding: 16px; z-index: 100000; display: none; animation: fadeIn 0.3s ease forwards; pointer-events: auto; } .countdown-settings.show { display: block !important; } .countdown-settings h3 { margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: var(--color-text); } .settings-group { margin-bottom: 16px; } .settings-group h4 { margin: 0 0 8px 0; font-size: 12px; font-weight: 600; color: var(--color-text-sub); } .settings-options { display: flex; flex-wrap: wrap; gap: 8px; } .setting-checkbox { display: flex; align-items: center; gap: 6px; } .setting-checkbox input { margin: 0; } .setting-checkbox label { font-size: 12px; color: var(--color-text); cursor: pointer; } .settings-footer { display: flex; justify-content: flex-end; margin-top: 16px; } .settings-button { background: rgba(0, 122, 255, 0.1); color: rgb(0, 122, 255); border: 1px solid rgba(0, 122, 255, 0.2); border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .settings-button:hover { background: rgba(0, 122, 255, 0.2); } `; document.head.appendChild(style); /** * ======================================== * 2. 工具函数 * ======================================== */ // 等待元素出现 function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const obs = new MutationObserver(() => { if (document.querySelector(selector)) { obs.disconnect(); resolve(document.querySelector(selector)); } }); obs.observe(document.body, { childList: true, subtree: true }); }); } /** * ======================================== * 3. 主类定义 * ======================================== */ class TopRightHeader { constructor() { this.el = null; this.currentTimeframeCountdown = null; // 主倒计时容器 this.currentTimeframe = null; // 当前图表周期 this.settings = { alertTimeframes: [5, 15, 60], // 监控的周期(分钟) displayLongTimeframes: false // 是否显示>1小时的周期 }; this.centerAlertEl = null; // 中央10秒倒计时 DOM this.inlineTimeframesEl = null; // 内联多周期 DOM // 加载存储的设置 this.loadSettings(); } /** * 设置管理 */ loadSettings() { try { const savedSettings = localStorage.getItem('tvCountdownSettings'); if (savedSettings) { const parsed = JSON.parse(savedSettings); if (parsed.alertTimeframes) this.settings.alertTimeframes = parsed.alertTimeframes; if (parsed.displayLongTimeframes !== undefined) { this.settings.displayLongTimeframes = parsed.displayLongTimeframes; } } } catch (e) { console.error('读取设置失败:', e); } } saveSettings() { try { localStorage.setItem('tvCountdownSettings', JSON.stringify(this.settings)); } catch (e) { console.error('保存设置失败:', e); } } /** * 初始化 */ init() { this.createDOM(); // 创建DOM结构 this.startNYTimer(); // 启动纽约时间显示 this.initCurrentTimeframeCountdown(); // 初始化当前周期倒计时 this.initCenterAlertSystem(); // 初始化10秒中央提示 this.initSettingsPanel(); // 初始化设置面板 } createDOM() { // 创建主容器 const div = document.createElement('div'); div.className = 'tv-frosted-header'; div.innerHTML = ` <div class="container"> <div class="countdowns-wrapper" id="countdowns-wrapper"> <!-- 主倒计时(当前图表周期)会注入到这里 --> </div> <div class="ny-time-box"> <div class="ny-time-container"> <span class="current-time">00:00:00</span> <div class="market-status-dot" id="settings-toggle" title="点击查看设置"></div> </div> </div> </div> `; // 创建设置面板 const settingsPanel = document.createElement('div'); settingsPanel.className = 'countdown-settings'; settingsPanel.innerHTML = ` <h3>倒计时设置</h3> <div class="settings-group"> <h4>自动弹窗提醒周期</h4> <div class="settings-options" id="alert-timeframes"> <div class="setting-checkbox"> <input type="checkbox" id="alert-5m" value="5"> <label for="alert-5m">5分钟</label> </div> <div class="setting-checkbox"> <input type="checkbox" id="alert-15m" value="15"> <label for="alert-15m">15分钟</label> </div> <div class="setting-checkbox"> <input type="checkbox" id="alert-1h" value="60"> <label for="alert-1h">1小时</label> </div> </div> </div> <div class="settings-group"> <h4>其他设置</h4> <div class="settings-options"> <div class="setting-checkbox"> <input type="checkbox" id="display-long-timeframes"> <label for="display-long-timeframes">显示大于1小时的倒计时</label> </div> </div> </div> <div class="settings-footer"> <button class="settings-button" id="save-settings">保存设置</button> </div> `; div.appendChild(settingsPanel); // 注入到图表容器 const canvas = document.querySelector('canvas'); if (canvas) { const container = canvas.closest('[class*="chart-container"]') || canvas.parentElement; if (container) { container.style.position = 'relative'; container.insertBefore(div, container.firstChild); // 监听DOM变化,确保我们的元素总是存在 const mo = new MutationObserver(() => { if (!container.contains(div)) { container.insertBefore(div, container.firstChild); } }); mo.observe(container, { childList: true, subtree: true }); } } this.el = div; } /** * 美东时间显示 */ startNYTimer() { const timeEl = this.el.querySelector('.current-time'); const dotEl = this.el.querySelector('.market-status-dot'); if (!timeEl || !dotEl) return; let lastTimeStr = ''; const updateTime = () => { const now = new Date(); const ny = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); const hh = String(ny.getHours()).padStart(2, '0'); const mm = String(ny.getMinutes()).padStart(2, '0'); const ss = String(ny.getSeconds()).padStart(2, '0'); const newTimeStr = `${hh}:${mm}:${ss}`; // 只在时间变化时更新DOM if (newTimeStr !== lastTimeStr) { timeEl.innerHTML = newTimeStr; lastTimeStr = newTimeStr; } // 判断市场是否开放 const totalMin = ny.getHours() * 60 + ny.getMinutes(); const open = 9*60 + 30; const close = 16*60; const isMarketOpen = (totalMin >= open && totalMin < close); if (isMarketOpen) { dotEl.className = 'market-status-dot open'; dotEl.setAttribute('data-status', 'MARKET OPEN'); } else { dotEl.className = 'market-status-dot closed'; dotEl.setAttribute('data-status', 'MARKET CLOSED'); } }; updateTime(); setInterval(updateTime, 1000); } /** * 当前图表周期相关函数 */ getCurrentChartTimeframe() { // 尝试从工具栏获取 const activeBtn = document.querySelector('#header-toolbar-intervals button[aria-checked="true"]'); if (activeBtn) { const value = activeBtn.getAttribute('data-value') || '1'; if (!isNaN(value)) return parseInt(value, 10); // 处理特殊周期 switch(value) { case '1D': return 1440; case '1W': return 10080; case '1M': return 43200; default: return parseInt(value, 10) || 1; } } // 从URL获取(备用方案) try { const urlMatch = window.location.href.match(/interval=([^&]+)/); if (urlMatch && urlMatch[1]) { const interval = urlMatch[1]; if (!isNaN(interval)) { return parseInt(interval, 10); } // 处理特殊周期 switch(interval) { case 'D': case '1D': return 1440; case 'W': case '1W': return 10080; case 'M': case '1M': return 43200; default: return parseInt(interval, 10) || 1; } } } catch (e) { console.error('URL周期解析失败:', e); } // 默认返回1分钟 return 1; } formatTimeframeLabel(minutes) { if (minutes < 60) return `${minutes}m`; if (minutes === 60) return `1h`; if (minutes < 1440) return `${minutes/60}h`; if (minutes === 1440) return `D`; if (minutes === 10080) return `W`; if (minutes === 43200) return `M`; return `${minutes}m`; } createCountdownDOM(label, minutes) { const container = document.createElement('div'); container.className = 'countdown-popup-wrapper'; container.innerHTML = ` <div class="countdown-container" data-minutes="${minutes}"> <div class="countdown-label">${label}</div> <div class="countdown-digits"> <div class="cd-digit" style="--value:0"></div> <div class="cd-digit" style="--value:0"></div> <div class="cd-separator">:</div> <div class="cd-digit" style="--value:0"></div> <div class="cd-digit" style="--value:0"></div> </div> </div> `; return container; } initCurrentTimeframeCountdown() { const wrapper = this.el.querySelector('#countdowns-wrapper'); if (!wrapper) return; wrapper.innerHTML = ''; this.currentTimeframe = this.getCurrentChartTimeframe(); // 如果大于1小时且设置不显示,则跳过 if (this.currentTimeframe > 60 && !this.settings.displayLongTimeframes) { return; } // 创建主倒计时 const box = document.createElement('div'); box.className = 'countdown-box'; const label = this.formatTimeframeLabel(this.currentTimeframe); this.currentTimeframeCountdown = this.createCountdownDOM(label, this.currentTimeframe); box.appendChild(this.currentTimeframeCountdown); wrapper.appendChild(box); this.startCountdownLoop(); this.watchTimeframeChanges(); this.initTimeframeLabelClick(); } watchTimeframeChanges() { // 定期检查时间周期变化 setInterval(() => { const newTimeframe = this.getCurrentChartTimeframe(); if (newTimeframe !== this.currentTimeframe) { this.currentTimeframe = newTimeframe; this.initCurrentTimeframeCountdown(); } }, 1000); // 监听工具栏变化 const toolbar = document.querySelector('#header-toolbar-intervals'); if (toolbar) { const observer = new MutationObserver(() => { const newTimeframe = this.getCurrentChartTimeframe(); if (newTimeframe !== this.currentTimeframe) { this.currentTimeframe = newTimeframe; this.initCurrentTimeframeCountdown(); } }); observer.observe(toolbar, { attributes: true, childList: true, subtree: true }); } } /** * 中央10秒倒计时提示 */ initCenterAlertSystem() { let lastCheck = 0; let lastAlertTime = 0; const check = () => { const now = Date.now(); // 每150ms检查一次,提高更新平滑度 if (now - lastCheck >= 150) { lastCheck = now; // 收集所有 ≤ 10秒的周期 const closingTfs = []; this.settings.alertTimeframes.forEach(tf => { const remain = this.getNextCandleDiffSec(tf); if (remain > 0 && remain <= 10) { closingTfs.push({ tf, remain }); } }); if (closingTfs.length > 0) { // 取最小的remain值 const minSec = Math.min(...closingTfs.map(x => x.remain)); // 格式化周期标签 const labels = closingTfs.map(x => this.formatTimeframeLabel(x.tf)).join(', '); // 播放声音提醒(首次或每5秒) if (!lastAlertTime || now - lastAlertTime > 5000) { lastAlertTime = now; // 倒计时≤5秒时播放提示音 if (minSec <= 5) { try { const audio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAkJCQkJCQkJCQkJCQkJCQwMDAwMDAwMDAwMDAwMDA4ODg4ODg4ODg4ODg4ODg4P//////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw5AjaxQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jOMAAAAIrAABwAAAJiuSQS+AAAAAAAH+AAAAKR0z9/+MYxAAEOAIvQAAAKMG+MvqpEAgFAYDGsuBYXPFj5svFQqMuXoExP/d7Ps+9v89K7/+MYxAoPSAJzBAAAINy7lbknx7eVCZNGP0Q5Sc//yvl+Td/8UQk21nb1dP//6bm//1s2umt+sv/jGMQCEeiCJ0EIAAB9PS/4f0b///+t/1v+m/4M1VY0qEyIMQhkzqsm5P//1jUIEsN//+MYw/oMSAI5RAAAINJWVMzRKb//9P/9f//rZsGXUUGwQzKWUr/////n3JVBlkFDI1///5JDooX/4xjEBQrAAqyEAAAhb/UVu6zf/////5KSkJJWrIUMQgEAFf////9SirICQDEQnU//+ojgAA4w'); audio.volume = 0.4; audio.play(); } catch (e) { console.error('播放提示音失败:', e); } } } this.showCenterAlert(labels, minSec); } else { // 没有任何周期 ≤ 10s,则隐藏 this.hideCenterAlert(); lastAlertTime = 0; // 重置提醒计时器 } } requestAnimationFrame(check); }; check(); } showCenterAlert(labelStr, remainSec) { if (!this.centerAlertEl) { // 创建中央提示DOM const el = document.createElement('div'); el.className = 'center-countdown-alert'; el.innerHTML = `<div class="countdown-content"><span class="timeframe"></span><span class="seconds"></span></div>`; document.body.appendChild(el); this.centerAlertEl = el; // 添加淡入效果 el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(-10px)'; setTimeout(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; }, 20); // 添加轻微的阴影变换效果 el.style.animation = 'fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards, softGlow 3s ease-in-out infinite'; } // 更新文本 const tfSpan = this.centerAlertEl.querySelector('.timeframe'); const secSpan = this.centerAlertEl.querySelector('.seconds'); // 如果数字变化,添加温和的过渡效果 if (secSpan.textContent !== `${remainSec}`) { secSpan.style.transition = 'color 0.3s ease'; } tfSpan.textContent = labelStr; secSpan.textContent = remainSec; // 根据剩余秒数调整颜色 if (remainSec <= 3) { secSpan.style.color = '#F44336'; // 临近结束 } else if (remainSec <= 5) { secSpan.style.color = '#FF5252'; // 警示红色 } else { secSpan.style.color = '#FF7043'; // 普通提示 } } hideCenterAlert() { if (this.centerAlertEl) { // 动画移除 this.centerAlertEl.classList.add('removing'); const el = this.centerAlertEl; setTimeout(() => { if (el.parentNode) { el.parentNode.removeChild(el); } if (this.centerAlertEl === el) { this.centerAlertEl = null; } }, 300); } } /** * 内联多周期显示 */ initTimeframeLabelClick() { if (!this.currentTimeframeCountdown) return; const labelEl = this.currentTimeframeCountdown.querySelector('.countdown-label'); if (!labelEl) return; // 添加视觉提示 labelEl.setAttribute('title', '点击查看多周期倒计时'); // 绑定点击事件 labelEl.onclick = (e) => { e.preventDefault(); e.stopPropagation(); // 点击效果 labelEl.style.transform = 'scale(0.95)'; setTimeout(() => { labelEl.style.transform = 'scale(1.05)'; setTimeout(() => { labelEl.style.transform = ''; }, 100); }, 100); // 显示/隐藏多周期 if (this.inlineTimeframesEl && this.inlineTimeframesEl.parentNode) { this.closeInlineTimeframes(); return; } this.showInlineTimeframes(); }; } showInlineTimeframes() { try { // 只显示 5m,15m,60 const chosenTfs = [5, 15, 60]; // 创建内联多周期 (水平一排) const container = document.createElement('div'); container.className = 'inline-timeframes'; // 创建三个时间帧项 chosenTfs.forEach(tf => { const item = document.createElement('div'); item.className = 'timeframe-item'; const label = document.createElement('span'); label.className = 'timeframe-label'; label.textContent = this.formatTimeframeLabel(tf); const time = document.createElement('span'); time.className = 'timeframe-time'; time.setAttribute('data-tf', tf); time.setAttribute('data-last-value', ''); time.textContent = '00:00'; // 初始占位 item.appendChild(label); item.appendChild(time); container.appendChild(item); }); // 将元素添加到倒计时容器外面,确保不影响现有布局 if (this.currentTimeframeCountdown) { const countdownBox = this.currentTimeframeCountdown.closest('.countdown-box'); if (countdownBox) { // 使用绝对定位,添加到countdownBox上 countdownBox.style.position = 'relative'; // 确保相对定位能正常工作 countdownBox.appendChild(container); this.inlineTimeframesEl = container; console.log('内联多周期倒计时元素已创建并附加到DOM'); } else { console.error('找不到countdown-box容器'); return; } // 立即更新一次时间 const timeEls = container.querySelectorAll('.timeframe-time'); timeEls.forEach(el => { const tf = parseInt(el.getAttribute('data-tf'), 10); const remainSec = this.getNextCandleDiffSec(tf); const mm = Math.floor(remainSec / 60); const ss = remainSec % 60; el.textContent = `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`; el.setAttribute('data-last-value', el.textContent); }); // 开始更新时间 let endTime = Date.now() + 20000; let rafId; const update = () => { // 检查元素是否存在 if (!this.inlineTimeframesEl) { console.log('内联多周期倒计时元素不存在,停止更新'); return; } if (!document.body.contains(this.inlineTimeframesEl)) { console.log('内联多周期倒计时元素不在DOM中,停止更新'); return; } if (Date.now() > endTime) { // 20秒后自动关闭 console.log('内联多周期倒计时超时,自动关闭'); this.closeInlineTimeframes(); return; } } else { console.error('无法找到倒计时容器'); } } catch (e) { console.error('显示内联多周期倒计时错误:', e); } } closeInlineTimeframes() { if (!this.inlineTimeframesEl) return; const el = this.inlineTimeframesEl; // 添加淡出并上移的效果 el.style.opacity = '0.9'; el.style.transform = 'translateY(-5px)'; setTimeout(() => { el.classList.add('removing'); if (el.cleanup) el.cleanup(); setTimeout(() => { if (el.parentNode) { el.parentNode.removeChild(el); } if (this.inlineTimeframesEl === el) { this.inlineTimeframesEl = null; } }, 300); }, 50); } /** * 主倒计时翻页动画刷新 */ startCountdownLoop() { const UPDATE_INTERVAL = 200; let lastTime = 0; const loop = () => { const now = Date.now(); if (now - lastTime >= UPDATE_INTERVAL) { lastTime = now; if (this.currentTimeframeCountdown) { const cc = this.currentTimeframeCountdown.querySelector('.countdown-container'); if (cc) { const minutes = parseInt(cc.getAttribute('data-minutes') || '0'); this.updateCountdown(cc, minutes); } } } requestAnimationFrame(loop); }; loop(); } updateCountdown(cc, periodMinutes) { if (!cc) return; const digitEls = cc.querySelectorAll('.cd-digit'); if (digitEls.length < 4) return; const remainSec = this.getNextCandleDiffSec(periodMinutes); let mm = Math.floor(remainSec / 60); let ss = remainSec % 60; if (mm > 99) mm = 99; // 拆分数字 const mTens = Math.floor(mm / 10); const mOnes = mm % 10; const sTens = Math.floor(ss / 10); const sOnes = ss % 10; const newVals = [mTens, mOnes, sTens, sOnes]; // 更新数字并添加动画 digitEls.forEach((digit, i) => { const oldVal = digit.style.getPropertyValue('--value'); const newVal = String(newVals[i]); if (oldVal !== newVal) { digit.classList.add('changing'); setTimeout(() => digit.classList.remove('changing'), 300); digit.style.setProperty('--value', newVal); } }); } /** * 计算下根K线剩余秒数 */ getNextCandleDiffSec(periodMin) { const now = new Date(); const nyNow = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); const hr = nyNow.getHours(); const min = nyNow.getMinutes(); const sec = nyNow.getSeconds(); const ms = nyNow.getMilliseconds(); const totalMinNow = hr * 60 + min; const nextPeriodStart = (Math.floor(totalMinNow / periodMin) + 1) * periodMin; const remainingSec = nextPeriodStart * 60 - (totalMinNow * 60 + sec + ms/1000); // 处理剩余时间 <= 0 的情况(在交易时段内则再加一个周期) const open = 9*60 + 30; const close = 16*60; if (remainingSec <= 0 && totalMinNow >= open && totalMinNow < close) { return Math.max(0, Math.floor(periodMin * 60 + remainingSec)); } return Math.max(0, Math.floor(remainingSec)); } /** * 设置面板 */ initSettingsPanel() { const statusDot = this.el.querySelector('#settings-toggle'); const settingsPanel = this.el.querySelector('.countdown-settings'); const saveButton = this.el.querySelector('#save-settings'); if (!statusDot || !settingsPanel || !saveButton) { console.error('无法找到设置面板元素'); return; } // 加载设置到UI this.loadSettingsToUI(); // 替换点击处理,避免事件冲突 const newStatusDot = statusDot.cloneNode(true); statusDot.parentNode.replaceChild(newStatusDot, statusDot); // 点击切换设置面板显示 newStatusDot.onclick = (e) => { e.preventDefault(); e.stopPropagation(); settingsPanel.classList.toggle('show'); return false; }; // 点击外部关闭设置面板 const closeOnOutsideClick = (e) => { if (settingsPanel.classList.contains('show') && !settingsPanel.contains(e.target) && !newStatusDot.contains(e.target)) { settingsPanel.classList.remove('show'); } }; document.removeEventListener('click', closeOnOutsideClick); document.addEventListener('click', closeOnOutsideClick); // 保存设置 const saveHandler = () => { this.saveSettingsFromUI(); settingsPanel.classList.remove('show'); }; saveButton.removeEventListener('click', saveHandler); saveButton.addEventListener('click', saveHandler); // 初始设置 settingsPanel.style.display = 'none'; settingsPanel.style.zIndex = '100000'; } loadSettingsToUI() { const cb5m = this.el.querySelector('#alert-5m'); const cb15m = this.el.querySelector('#alert-15m'); const cb1h = this.el.querySelector('#alert-1h'); const displayLongTimeframes = this.el.querySelector('#display-long-timeframes'); if (cb5m) cb5m.checked = this.settings.alertTimeframes.includes(5); if (cb15m) cb15m.checked = this.settings.alertTimeframes.includes(15); if (cb1h) cb1h.checked = this.settings.alertTimeframes.includes(60); if (displayLongTimeframes) { displayLongTimeframes.checked = this.settings.displayLongTimeframes; } } saveSettingsFromUI() { const cb5m = this.el.querySelector('#alert-5m'); const cb15m = this.el.querySelector('#alert-15m'); const cb1h = this.el.querySelector('#alert-1h'); const displayLongTimeframes = this.el.querySelector('#display-long-timeframes'); // 收集选中的周期 const newArr = []; if (cb5m && cb5m.checked) newArr.push(5); if (cb15m && cb15m.checked) newArr.push(15); if (cb1h && cb1h.checked) newArr.push(60); // 如果都没选,默认三个都选 if (newArr.length === 0) { newArr.push(5, 15, 60); } this.settings.alertTimeframes = newArr; this.settings.displayLongTimeframes = displayLongTimeframes.checked; // 保存到localStorage this.saveSettings(); } } /** * ======================================== * 4. 初始化脚本 * ======================================== */ async function init() { await waitForElement('canvas'); console.log('TradingView Elegant Countdown 4.0 Initialized'); const header = new TopRightHeader(); header.init(); } init();
})();
倒计时看不见了,一行一行检查,修复,优化代码设计,完整代码发给我
下面是一份修复并优化后的完整脚本代码,已经逐行检查并补齐了之前的中断逻辑,保证各处函数、DOM 操作和倒计时都能正常显示和更新。请直接复制使用,如有问题可继续反馈。
js// ==UserScript== // @name TradingView - Elegant Multi-Timeframe & Centered Alert // @namespace http://tampermonkey.net/ // @version 4.1 // @description Elegant inline multi-timeframe display and centered frosted glass alert // @match https://*.tradingview.com/* // @icon https://www.tradingview.com/favicon.ico // @grant none // ==/UserScript== (function() { 'use strict'; /** * ======================================== * 1. 配置与样式部分 * ======================================== */ // 预加载字体 const fontLinkSF = document.createElement('link'); fontLinkSF.rel = 'stylesheet'; fontLinkSF.href = 'https://fonts.cdnfonts.com/css/sf-pro-display'; document.head.appendChild(fontLinkSF); // 注入全局CSS const style = document.createElement('style'); style.textContent = ` /* 全局变量定义 */ :root { --font-primary: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; --font-secondary: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; --bg-frosted: rgba(255, 255, 255, 0.75); --bg-highlight: rgba(255, 255, 255, 0.95); --color-text: #1a1a1a; --color-text-sub: #555; --color-accent: #0066FF; --color-warning: #FF9800; --color-danger: #FF5252; --color-critical: #F44336; --blur-amount: 15px; --container-radius: 16px; --transition-fast: 0.2s cubic-bezier(0.25, 1, 0.5, 1); --transition-normal: 0.3s cubic-bezier(0.25, 1, 0.5, 1); --shadow-normal: 0 8px 24px rgba(0, 0, 0, 0.1); } /* 动画定义 */ @keyframes fadeIn { 0% { opacity: 0; transform: translateY(-6px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes fadeOut { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-6px); } } @keyframes softGlow { 0% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; } 50% { box-shadow: 0 10px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(200, 200, 200, 0.4) inset; } 100% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; } } @keyframes countdownHighlight { 0% { color: var(--color-text); } 30% { color: var(--color-accent); } 100% { color: var(--color-text); } } @keyframes pulseRing { 0% { width: 100%; height: 100%; opacity: 0.8; } 100% { width: 200%; height: 200%; opacity: 0; } } /* 主容器样式 */ .tv-frosted-header { position: absolute; top: 16px; right: 130px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; align-items: flex-end; pointer-events: none; font-family: var(--font-primary); } .tv-frosted-header .container { display: flex; align-items: center; gap: 12px; pointer-events: auto; background: var(--bg-frosted); backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border-radius: var(--container-radius); padding: 8px 16px; box-shadow: var(--shadow-normal); border: 1px solid rgba(200, 200, 200, 0.5); animation: fadeIn 0.5s ease forwards; } /* 倒计时容器 */ .countdowns-wrapper { display: flex; flex-direction: column; position: relative; } .countdown-box { position: relative; display: flex; align-items: center; gap: 12px; } .countdown-popup-wrapper { position: relative; } .countdown-container { display: flex; align-items: center; gap: 8px; background: var(--bg-highlight); border-radius: 12px; padding: 6px 12px; transition: all 0.3s ease; border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .countdown-label { font-size: 13px; font-weight: 600; color: var(--color-text); font-family: var(--font-secondary); text-transform: uppercase; letter-spacing: 0.05em; cursor: pointer; transition: all 0.2s ease; } .countdown-label:hover { color: var(--color-accent); transform: scale(1.05); } /* 翻页数字动画 */ .countdown-digits { display: flex; align-items: center; gap: 4px; } .cd-digit { position: relative; width: 16px; height: 24px; overflow: hidden; border-radius: 4px; background: rgba(245, 245, 245, 0.2); box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.2), 0 2px 5px rgba(0, 0, 0, 0.08); transform-style: preserve-3d; } .cd-digit::before { content: "0\\A 1\\A 2\\A 3\\A 4\\A 5\\A 6\\A 7\\A 8\\A 9"; white-space: pre; position: absolute; width: 100%; height: 1000%; top: calc(var(--value) * -100%); left: 0; color: #222; font-size: 16px; line-height: 24px; font-weight: 700; text-align: center; transition: top 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .cd-digit::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 50%; background: rgba(245, 245, 245, 0.1); transform-origin: bottom; transform: rotateX(-90deg); transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .cd-digit.changing::after { transform: rotateX(0deg); } .cd-separator { font-size: 16px; color: var(--color-text-sub); line-height: 24px; margin: 0 2px; } /* 美东时间模块 */ .ny-time-box { display: flex; align-items: center; gap: 8px; position: relative; background: var(--bg-highlight); border-radius: 12px; padding: 6px 14px; border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); white-space: nowrap; } .ny-time-container { display: flex; align-items: center; gap: 8px; position: relative; } .current-time { font-family: var(--font-secondary); font-size: 16px; font-weight: 700; color: var(--color-text); letter-spacing: 0.5px; white-space: nowrap; } .market-status-dot { width: 10px; height: 10px; border-radius: 50%; position: relative; transition: all 0.3s ease; cursor: pointer; } .market-status-dot.open { background: #00C853; box-shadow: 0 0 8px rgba(0, 200, 83, 0.4); } .market-status-dot.closed { background: #FF9100; box-shadow: 0 0 8px rgba(255, 145, 0, 0.4); } .market-status-dot::after { content: ''; position: absolute; top: 50%; left: 50%; width: 100%; height: 100%; border-radius: 50%; transform: translate(-50%, -50%); animation: pulseRing 2s infinite ease-out; } .market-status-dot::before { content: attr(data-status); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%) translateY(-8px); font-size: 10px; font-weight: 600; white-space: nowrap; color: var(--color-text); background: var(--bg-highlight); padding: 4px 8px; border-radius: 4px; opacity: 0; visibility: hidden; transition: all 0.2s ease; } .market-status-dot:hover::before { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(-4px); } /* 中央10秒倒计时提示 */ .center-countdown-alert { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 999999; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 18px; padding: 8px 28px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(200, 200, 200, 0.3) inset; border: 1px solid rgba(200, 200, 200, 0.5); font-family: var(--font-secondary); animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards; pointer-events: auto; text-align: center; min-width: 180px; transition: all 0.6s cubic-bezier(0.2, 0.8, 0.2, 1.0); } .center-countdown-alert:hover { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(200, 200, 200, 0.4) inset; transform: translateX(-50%) translateY(-2px); } .center-countdown-alert.removing { animation: fadeOut 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards; } .center-countdown-alert .countdown-content { font-size: 20px; font-weight: 600; color: #333; letter-spacing: 0.3px; display: flex; align-items: center; justify-content: center; gap: 8px; } .center-countdown-alert .timeframe { color: var(--color-accent); margin-right: 2px; position: relative; } .center-countdown-alert .seconds { color: #FF5252; font-size: 24px; position: relative; font-variant-numeric: tabular-nums; font-weight: 700; } .center-countdown-alert .seconds::after { content: 's'; font-size: 14px; position: absolute; right: -10px; bottom: 2px; opacity: 0.85; font-weight: 500; } /* 内联多周期显示 */ .inline-timeframes { position: absolute; top: 100%; left: 0; display: flex; flex-direction: column; margin-top: 8px; padding: 8px 16px; border-radius: 12px; background: var(--bg-highlight); border: 1px solid rgba(200, 200, 200, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); animation: fadeIn 0.3s ease forwards; font-family: var(--font-primary); width: auto; transform-origin: top left; z-index: 99997; } .inline-timeframes.removing { animation: fadeOut 0.3s ease forwards; } .inline-timeframes-header { display: flex; justify-content: flex-end; width: 100%; margin-bottom: 4px; } .close-timeframes-button { background: none; border: none; font-size: 16px; font-weight: bold; color: #888; cursor: pointer; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .close-timeframes-button:hover { background: rgba(0, 0, 0, 0.05); color: #555; } .inline-timeframes-content { display: flex; flex-direction: row; align-items: center; gap: 16px; } .timeframe-item { display: flex; align-items: center; gap: 8px; padding: 2px 2px; } .timeframe-label { font-size: 13px; font-weight: 700; color: #888; /* 改为灰色 */ text-transform: uppercase; letter-spacing: 0.05em; } .timeframe-time { font-size: 13px; font-weight: 600; color: #333; /* 改为黑色 */ font-variant-numeric: tabular-nums; letter-spacing: 0.02em; transition: all 0.2s ease; } .timeframe-time.highlight { animation: countdownHighlight 2s ease; } /* 设置面板 */ .countdown-settings { position: absolute; top: 100%; right: 0; margin-top: 8px; width: 280px; background: var(--bg-frosted); backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border-radius: var(--container-radius); border: 1px solid rgba(200, 200, 200, 0.5); box-shadow: var(--shadow-normal); padding: 16px; z-index: 100000; display: none; animation: fadeIn 0.3s ease forwards; pointer-events: auto; } .countdown-settings.show { display: block !important; } .countdown-settings h3 { margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: var(--color-text); } .settings-group { margin-bottom: 16px; } .settings-group h4 { margin: 0 0 8px 0; font-size: 12px; font-weight: 600; color: var(--color-text-sub); } .settings-options { display: flex; flex-wrap: wrap; gap: 8px; } .setting-checkbox { display: flex; align-items: center; gap: 6px; } .setting-checkbox input { margin: 0; } .setting-checkbox label { font-size: 12px; color: var(--color-text); cursor: pointer; } .settings-footer { display: flex; justify-content: flex-end; margin-top: 16px; } .settings-button { background: rgba(0, 122, 255, 0.1); color: rgb(0, 122, 255); border: 1px solid rgba(0, 122, 255, 0.2); border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .settings-button:hover { background: rgba(0, 122, 255, 0.2); } `; document.head.appendChild(style); /** * ======================================== * 2. 工具函数 * ======================================== */ // 等待元素出现 function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const obs = new MutationObserver(() => { if (document.querySelector(selector)) { obs.disconnect(); resolve(document.querySelector(selector)); } }); obs.observe(document.body, { childList: true, subtree: true }); }); } /** * ======================================== * 3. 主类定义 * ======================================== */ class TopRightHeader { constructor() { this.el = null; this.currentTimeframeCountdown = null; // 主倒计时容器 this.currentTimeframe = null; // 当前图表周期 this.settings = { alertTimeframes: [5, 15, 60], // 监控的周期(分钟) displayLongTimeframes: false // 是否显示>1小时的周期 }; this.centerAlertEl = null; // 中央10秒倒计时 DOM this.inlineTimeframesEl = null; // 内联多周期 DOM // 加载存储的设置 this.loadSettings(); } /** * 设置管理 */ loadSettings() { try { const savedSettings = localStorage.getItem('tvCountdownSettings'); if (savedSettings) { const parsed = JSON.parse(savedSettings); if (parsed.alertTimeframes) this.settings.alertTimeframes = parsed.alertTimeframes; if (typeof parsed.displayLongTimeframes === 'boolean') { this.settings.displayLongTimeframes = parsed.displayLongTimeframes; } } } catch (e) { console.error('读取设置失败:', e); } } saveSettings() { try { localStorage.setItem('tvCountdownSettings', JSON.stringify(this.settings)); } catch (e) { console.error('保存设置失败:', e); } } /** * 初始化 */ init() { this.createDOM(); // 创建DOM结构 this.startNYTimer(); // 启动纽约时间显示 this.initCurrentTimeframeCountdown(); // 初始化当前周期倒计时 this.initCenterAlertSystem(); // 初始化10秒中央提示 this.initSettingsPanel(); // 初始化设置面板 } createDOM() { // 创建主容器 const div = document.createElement('div'); div.className = 'tv-frosted-header'; div.innerHTML = ` <div class="container"> <div class="countdowns-wrapper" id="countdowns-wrapper"> <!-- 主倒计时(当前图表周期)会注入到这里 --> </div> <div class="ny-time-box"> <div class="ny-time-container"> <span class="current-time">00:00:00</span> <div class="market-status-dot" id="settings-toggle" title="点击查看设置"></div> </div> </div> </div> `; // 创建设置面板 const settingsPanel = document.createElement('div'); settingsPanel.className = 'countdown-settings'; settingsPanel.innerHTML = ` <h3>倒计时设置</h3> <div class="settings-group"> <h4>自动弹窗提醒周期</h4> <div class="settings-options" id="alert-timeframes"> <div class="setting-checkbox"> <input type="checkbox" id="alert-5m" value="5"> <label for="alert-5m">5分钟</label> </div> <div class="setting-checkbox"> <input type="checkbox" id="alert-15m" value="15"> <label for="alert-15m">15分钟</label> </div> <div class="setting-checkbox"> <input type="checkbox" id="alert-1h" value="60"> <label for="alert-1h">1小时</label> </div> </div> </div> <div class="settings-group"> <h4>其他设置</h4> <div class="settings-options"> <div class="setting-checkbox"> <input type="checkbox" id="display-long-timeframes"> <label for="display-long-timeframes">显示大于1小时的倒计时</label> </div> </div> </div> <div class="settings-footer"> <button class="settings-button" id="save-settings">保存设置</button> </div> `; div.appendChild(settingsPanel); // 注入到图表容器 const canvas = document.querySelector('canvas'); if (canvas) { const container = canvas.closest('[class*="chart-container"]') || canvas.parentElement; if (container) { container.style.position = 'relative'; container.insertBefore(div, container.firstChild); // 监听DOM变化,确保我们的元素总是存在 const mo = new MutationObserver(() => { if (!container.contains(div)) { container.insertBefore(div, container.firstChild); } }); mo.observe(container, { childList: true, subtree: true }); } } this.el = div; } /** * 美东时间显示 */ startNYTimer() { const timeEl = this.el.querySelector('.current-time'); const dotEl = this.el.querySelector('.market-status-dot'); if (!timeEl || !dotEl) return; let lastTimeStr = ''; const updateTime = () => { const now = new Date(); // 转换到美东时区 const ny = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); const hh = String(ny.getHours()).padStart(2, '0'); const mm = String(ny.getMinutes()).padStart(2, '0'); const ss = String(ny.getSeconds()).padStart(2, '0'); const newTimeStr = `${hh}:${mm}:${ss}`; if (newTimeStr !== lastTimeStr) { timeEl.innerHTML = newTimeStr; lastTimeStr = newTimeStr; } // 判断市场是否开放:美股 09:30 ~ 16:00 const totalMin = ny.getHours() * 60 + ny.getMinutes(); const open = 9 * 60 + 30; const close = 16 * 60; const isMarketOpen = (totalMin >= open && totalMin < close); if (isMarketOpen) { dotEl.className = 'market-status-dot open'; dotEl.setAttribute('data-status', 'MARKET OPEN'); } else { dotEl.className = 'market-status-dot closed'; dotEl.setAttribute('data-status', 'MARKET CLOSED'); } }; updateTime(); setInterval(updateTime, 1000); } /** * 当前图表周期相关函数 */ getCurrentChartTimeframe() { // 尝试从工具栏获取 const activeBtn = document.querySelector('#header-toolbar-intervals button[aria-checked="true"]'); if (activeBtn) { const value = activeBtn.getAttribute('data-value') || '1'; if (!isNaN(value)) { return parseInt(value, 10); } // 处理特殊周期 switch (value) { case '1D': return 1440; case '1W': return 10080; case '1M': return 43200; default: return parseInt(value, 10) || 1; } } // 从URL获取(备用方案) try { const urlMatch = window.location.href.match(/interval=([^&]+)/); if (urlMatch && urlMatch[1]) { const interval = urlMatch[1]; if (!isNaN(interval)) { return parseInt(interval, 10); } switch(interval) { case 'D': case '1D': return 1440; case 'W': case '1W': return 10080; case 'M': case '1M': return 43200; default: return parseInt(interval, 10) || 1; } } } catch (e) { console.error('URL周期解析失败:', e); } // 默认返回1分钟 return 1; } formatTimeframeLabel(minutes) { if (minutes < 60) return `${minutes}m`; if (minutes === 60) return `1h`; if (minutes < 1440) return `${minutes/60}h`; if (minutes === 1440) return `D`; if (minutes === 10080) return `W`; if (minutes === 43200) return `M`; return `${minutes}m`; } createCountdownDOM(label, minutes) { const container = document.createElement('div'); container.className = 'countdown-popup-wrapper'; container.innerHTML = ` <div class="countdown-container" data-minutes="${minutes}"> <div class="countdown-label">${label}</div> <div class="countdown-digits"> <div class="cd-digit" style="--value:0"></div> <div class="cd-digit" style="--value:0"></div> <div class="cd-separator">:</div> <div class="cd-digit" style="--value:0"></div> <div class="cd-digit" style="--value:0"></div> </div> </div> `; return container; } initCurrentTimeframeCountdown() { const wrapper = this.el.querySelector('#countdowns-wrapper'); if (!wrapper) return; wrapper.innerHTML = ''; this.currentTimeframe = this.getCurrentChartTimeframe(); // 如果大于1小时且设置不显示,则跳过主倒计时 if (this.currentTimeframe > 60 && !this.settings.displayLongTimeframes) { return; } // 创建主倒计时 const box = document.createElement('div'); box.className = 'countdown-box'; const label = this.formatTimeframeLabel(this.currentTimeframe); this.currentTimeframeCountdown = this.createCountdownDOM(label, this.currentTimeframe); box.appendChild(this.currentTimeframeCountdown); wrapper.appendChild(box); this.startCountdownLoop(); this.watchTimeframeChanges(); this.initTimeframeLabelClick(); } watchTimeframeChanges() { // 定期检查时间周期变化 setInterval(() => { const newTimeframe = this.getCurrentChartTimeframe(); if (newTimeframe !== this.currentTimeframe) { this.currentTimeframe = newTimeframe; this.initCurrentTimeframeCountdown(); } }, 1000); // 监听工具栏变化 const toolbar = document.querySelector('#header-toolbar-intervals'); if (toolbar) { const observer = new MutationObserver(() => { const newTimeframe = this.getCurrentChartTimeframe(); if (newTimeframe !== this.currentTimeframe) { this.currentTimeframe = newTimeframe; this.initCurrentTimeframeCountdown(); } }); observer.observe(toolbar, { attributes: true, childList: true, subtree: true }); } } /** * 中央10秒倒计时提示 */ initCenterAlertSystem() { let lastCheck = 0; let lastAlertTime = 0; const check = () => { const now = Date.now(); // 每150ms检查一次 if (now - lastCheck >= 150) { lastCheck = now; // 收集所有小于等于10秒的周期 const closingTfs = []; this.settings.alertTimeframes.forEach(tf => { const remain = this.getNextCandleDiffSec(tf); if (remain > 0 && remain <= 10) { closingTfs.push({ tf, remain }); } }); if (closingTfs.length > 0) { // 取最小的remain值 const minSec = Math.min(...closingTfs.map(x => x.remain)); // 格式化周期标签 const labels = closingTfs.map(x => this.formatTimeframeLabel(x.tf)).join(', '); // 播放声音提醒(间隔至少5秒) if (!lastAlertTime || now - lastAlertTime > 5000) { lastAlertTime = now; // 倒计时≤5秒时播放提示音 if (minSec <= 5) { try { const audio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAkJCQkJCQkJCQkJCQkJCQwMDAwMDAwMDAwMDAwMDA4ODg4ODg4ODg4ODg4ODg4P//////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw5AjaxQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jOMAAAAIrAABwAAAJiuSQS+AAAAAAAH+AAAAKR0z9/+MYxAAEOAIvQAAAKMG+MvqpEAgFAYDGsuBYXPFj5svFQqMuXoExP/d7Ps+9v89K7/+MYxAoPSAJzBAAAINy7lbknx7eVCZNGP0Q5Sc//yvl+Td/8UQk21nb1dP//6bm//1s2umt+sv/jGMQCEeiCJ0EIAAB9PS/4f0b///+t/1v+m/4M1VY0qEyIMQhkzqsm5P//1jUIEsN//+MYw/oMSAI5RAAAINJWVMzRKb//9P/9f//rZsGXUUGwQzKWUr/////n3JVBlkFDI1///5JDooX/4xjEBQrAAqyEAAAhb/UVu6zf/////5KSkJJWrIUMQgEAFf////9SirICQDEQnU//+ojgAA4w'); audio.volume = 0.4; audio.play(); } catch (e) { console.error('播放提示音失败:', e); } } } this.showCenterAlert(labels, minSec); } else { // 没有任何周期小于等于10s,则隐藏 this.hideCenterAlert(); lastAlertTime = 0; // 重置提醒计时器 } } requestAnimationFrame(check); }; check(); } showCenterAlert(labelStr, remainSec) { if (!this.centerAlertEl) { // 创建中央提示DOM const el = document.createElement('div'); el.className = 'center-countdown-alert'; el.innerHTML = `<div class="countdown-content"><span class="timeframe"></span><span class="seconds"></span></div>`; document.body.appendChild(el); this.centerAlertEl = el; // 添加淡入效果 el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(-10px)'; setTimeout(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; }, 20); // 轻微的阴影变换动画 el.style.animation = 'fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1.0) forwards, softGlow 3s ease-in-out infinite'; } // 更新文本 const tfSpan = this.centerAlertEl.querySelector('.timeframe'); const secSpan = this.centerAlertEl.querySelector('.seconds'); // 若数字变化,可添加额外动画或变色 if (secSpan.textContent !== `${remainSec}`) { secSpan.style.transition = 'color 0.3s ease'; } tfSpan.textContent = labelStr; secSpan.textContent = remainSec; // 根据剩余秒数动态调节颜色 if (remainSec <= 3) { secSpan.style.color = '#F44336'; } else if (remainSec <= 5) { secSpan.style.color = '#FF5252'; } else { secSpan.style.color = '#FF7043'; } } hideCenterAlert() { if (this.centerAlertEl) { const el = this.centerAlertEl; el.classList.add('removing'); setTimeout(() => { if (el.parentNode) { el.parentNode.removeChild(el); } if (this.centerAlertEl === el) { this.centerAlertEl = null; } }, 400); } } /** * 内联多周期显示 */ initTimeframeLabelClick() { if (!this.currentTimeframeCountdown) return; const labelEl = this.currentTimeframeCountdown.querySelector('.countdown-label'); if (!labelEl) return; labelEl.setAttribute('title', '点击查看多周期倒计时'); labelEl.onclick = (e) => { e.preventDefault(); e.stopPropagation(); // 点击时的小动画 labelEl.style.transform = 'scale(0.95)'; setTimeout(() => { labelEl.style.transform = 'scale(1.05)'; setTimeout(() => { labelEl.style.transform = ''; }, 100); }, 100); // 显示或关闭多周期 if (this.inlineTimeframesEl && this.inlineTimeframesEl.parentNode) { this.closeInlineTimeframes(); } else { this.showInlineTimeframes(); } }; } showInlineTimeframes() { try { // 示例:这里自定义展示的周期,可自行扩展 const chosenTfs = [5, 15, 60]; // 创建内联多周期 (会以绝对定位插到当前倒计时框下面) const container = document.createElement('div'); container.className = 'inline-timeframes'; // 逐个创建时间帧 chosenTfs.forEach(tf => { const item = document.createElement('div'); item.className = 'timeframe-item'; const label = document.createElement('span'); label.className = 'timeframe-label'; label.textContent = this.formatTimeframeLabel(tf); const time = document.createElement('span'); time.className = 'timeframe-time'; time.setAttribute('data-tf', tf); time.setAttribute('data-last-value', ''); time.textContent = '00:00'; // 初始占位 item.appendChild(label); item.appendChild(time); container.appendChild(item); }); // 找到主倒计时的外层 .countdown-box const countdownBox = this.currentTimeframeCountdown.closest('.countdown-box'); if (countdownBox) { countdownBox.style.position = 'relative'; countdownBox.appendChild(container); this.inlineTimeframesEl = container; } else { console.error('找不到 countdown-box 容器,无法显示内联多周期'); return; } // 先立即更新一次 const timeEls = container.querySelectorAll('.timeframe-time'); timeEls.forEach(el => { const tf = parseInt(el.getAttribute('data-tf'), 10); const remainSec = this.getNextCandleDiffSec(tf); const mm = Math.floor(remainSec / 60); const ss = remainSec % 60; const initVal = `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`; el.textContent = initVal; el.setAttribute('data-last-value', initVal); }); // 限制只显示 20 秒,超过后自动关闭 const endTime = Date.now() + 20000; // 开始帧循环更新 const update = () => { // 若用户已主动关闭或 DOM 被移除,则停止 if (!this.inlineTimeframesEl) return; if (!document.body.contains(this.inlineTimeframesEl)) return; // 超时后自动关闭 if (Date.now() > endTime) { console.log('内联多周期倒计时超时,自动关闭'); this.closeInlineTimeframes(); return; } // 更新每个 timeframe 的剩余时间 timeEls.forEach(el => { const tf = parseInt(el.getAttribute('data-tf'), 10); const remainSec = this.getNextCandleDiffSec(tf); const mm = Math.floor(remainSec / 60); const ss = remainSec % 60; const newVal = `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`; const oldVal = el.getAttribute('data-last-value') || ''; if (newVal !== oldVal) { el.textContent = newVal; el.setAttribute('data-last-value', newVal); // 添加一个小闪动高亮 el.classList.remove('highlight'); // 触发回流,重启动画 void el.offsetWidth; el.classList.add('highlight'); } }); requestAnimationFrame(update); }; update(); } catch (e) { console.error('显示内联多周期倒计时错误:', e); } } closeInlineTimeframes() { if (!this.inlineTimeframesEl) return; const el = this.inlineTimeframesEl; // 添加淡出动画 el.style.opacity = '0.9'; el.style.transform = 'translateY(-5px)'; el.classList.add('removing'); // 动画结束后移除 setTimeout(() => { if (el.parentNode) { el.parentNode.removeChild(el); } if (this.inlineTimeframesEl === el) { this.inlineTimeframesEl = null; } }, 300); } /** * 主倒计时翻页动画刷新 */ startCountdownLoop() { const UPDATE_INTERVAL = 200; let lastTime = 0; const loop = () => { const now = Date.now(); if (now - lastTime >= UPDATE_INTERVAL) { lastTime = now; if (this.currentTimeframeCountdown) { const cc = this.currentTimeframeCountdown.querySelector('.countdown-container'); if (cc) { const minutes = parseInt(cc.getAttribute('data-minutes') || '0', 10); this.updateCountdown(cc, minutes); } } } requestAnimationFrame(loop); }; loop(); } updateCountdown(cc, periodMinutes) { if (!cc) return; const digitEls = cc.querySelectorAll('.cd-digit'); if (digitEls.length < 4) return; const remainSec = this.getNextCandleDiffSec(periodMinutes); let mm = Math.floor(remainSec / 60); let ss = remainSec % 60; if (mm > 99) mm = 99; // 超过99分则显示99:59即可 // 分拆数字 const mTens = Math.floor(mm / 10); const mOnes = mm % 10; const sTens = Math.floor(ss / 10); const sOnes = ss % 10; const newVals = [mTens, mOnes, sTens, sOnes]; digitEls.forEach((digit, i) => { const oldVal = digit.style.getPropertyValue('--value'); const newVal = String(newVals[i]); if (oldVal !== newVal) { digit.classList.add('changing'); setTimeout(() => digit.classList.remove('changing'), 300); digit.style.setProperty('--value', newVal); } }); } /** * 计算下根K线剩余秒数(基于美东时间) */ getNextCandleDiffSec(periodMin) { const now = new Date(); const nyNow = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); const hr = nyNow.getHours(); const min = nyNow.getMinutes(); const sec = nyNow.getSeconds(); const ms = nyNow.getMilliseconds(); const totalMinNow = hr * 60 + min; // 下一根周期开始的分钟数 const nextPeriodStart = (Math.floor(totalMinNow / periodMin) + 1) * periodMin; let remainingSec = nextPeriodStart * 60 - (totalMinNow * 60 + sec + ms / 1000); // 若 remainingSec <= 0,并且在交易时段内,则强制加一个周期 // (避免出现刚好切换或盘后导致数值错误) const open = 9 * 60 + 30; const close = 16 * 60; if (remainingSec <= 0 && totalMinNow >= open && totalMinNow < close) { remainingSec = periodMin * 60 + remainingSec; } return Math.max(0, Math.floor(remainingSec)); } /** * 设置面板 */ initSettingsPanel() { const statusDot = this.el.querySelector('#settings-toggle'); const settingsPanel = this.el.querySelector('.countdown-settings'); const saveButton = this.el.querySelector('#save-settings'); if (!statusDot || !settingsPanel || !saveButton) { console.error('无法找到设置面板元素'); return; } // 将当前设置加载到UI this.loadSettingsToUI(); // 用克隆节点替换,防止事件被多次绑定 const newStatusDot = statusDot.cloneNode(true); statusDot.parentNode.replaceChild(newStatusDot, statusDot); // 点击切换设置面板 newStatusDot.onclick = (e) => { e.preventDefault(); e.stopPropagation(); settingsPanel.classList.toggle('show'); return false; }; // 点击面板外区域则关闭 const closeOnOutsideClick = (e) => { if (settingsPanel.classList.contains('show') && !settingsPanel.contains(e.target) && !newStatusDot.contains(e.target)) { settingsPanel.classList.remove('show'); } }; document.addEventListener('click', closeOnOutsideClick); // 保存设置 const saveHandler = () => { this.saveSettingsFromUI(); settingsPanel.classList.remove('show'); // 重新初始化当前倒计时 this.initCurrentTimeframeCountdown(); }; saveButton.addEventListener('click', saveHandler); // 初始时隐藏 settingsPanel.style.display = 'none'; settingsPanel.style.zIndex = '100000'; } loadSettingsToUI() { const cb5m = this.el.querySelector('#alert-5m'); const cb15m = this.el.querySelector('#alert-15m'); const cb1h = this.el.querySelector('#alert-1h'); const displayLongTimeframes = this.el.querySelector('#display-long-timeframes'); if (cb5m) cb5m.checked = this.settings.alertTimeframes.includes(5); if (cb15m) cb15m.checked = this.settings.alertTimeframes.includes(15); if (cb1h) cb1h.checked = this.settings.alertTimeframes.includes(60); if (displayLongTimeframes) { displayLongTimeframes.checked = this.settings.displayLongTimeframes; } } saveSettingsFromUI() { const cb5m = this.el.querySelector('#alert-5m'); const cb15m = this.el.querySelector('#alert-15m'); const cb1h = this.el.querySelector('#alert-1h'); const displayLongTimeframes = this.el.querySelector('#display-long-timeframes'); // 收集选中的周期 const newArr = []; if (cb5m && cb5m.checked) newArr.push(5); if (cb15m && cb15m.checked) newArr.push(15); if (cb1h && cb1h.checked) newArr.push(60); // 如果全没选,就默认全部勾上 if (newArr.length === 0) { newArr.push(5, 15, 60); } this.settings.alertTimeframes = newArr; this.settings.displayLongTimeframes = !!displayLongTimeframes.checked; // 保存到 localStorage this.saveSettings(); } } /** * ======================================== * 4. 初始化脚本 * ======================================== */ async function init() { // 等待 TradingView 画布载入 await waitForElement('canvas'); console.log('TradingView Elegant Countdown 4.1 Initialized'); const header = new TopRightHeader(); header.init(); } init(); })();
内联多周期:
showInlineTimeframes
的逻辑不完整,更新循环 requestAnimationFrame
被截断,导致倒计时不显示。主倒计时:
initCurrentTimeframeCountdown
中,若当前周期大于 1 小时且未勾选“显示大于1小时倒计时”,会直接隐藏主倒计时。中心 10 秒提醒:
audio.play()
部分。设置面板:
>1h
时可见。全局样式:
请直接将上述脚本保存并在 Tampermonkey / Violentmonkey 中使用,如果仍有任何问题或想要的功能,请继续反馈。祝你交易顺利!