您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为Tradingview图表添加可自定义的背景图片,并支持透明度、位置、大小和适应模式的调整
// ==UserScript== // @name 交易视图自定义背景by X:@PPai_Crypto // @namespace http://tampermonkey.net/ // @version 1.4 // @description 为Tradingview图表添加可自定义的背景图片,并支持透明度、位置、大小和适应模式的调整 // @author X:@PPai_Crypto // @match https://www.tradingview.com/chart/* // @match https://cn.tradingview.com/chart/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // ==/UserScript== (function() { 'use strict'; // 调试函数 function log(message) { console.log(`[交易视图背景] ${message}`); } // 添加样式 GM_addStyle(` #custom-background-controls { position: absolute; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.7); color: white; padding: 10px; border-radius: 5px; z-index: 1000; display: none; /* 初始隐藏 */ } #custom-background-controls input, #custom-background-controls select, #custom-background-controls label { margin: 5px 0; display: block; } #custom-background-controls .preview { max-width: 100px; max-height: 100px; margin-top: 5px; } `); // 初始值 let currentImageUrl = GM_getValue('backgroundImageUrl', 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1350&q=80'); let currentOpacity = GM_getValue('backgroundOpacity', 30); // 0-100 let currentTop = GM_getValue('backgroundTop', 0); let currentLeft = GM_getValue('backgroundLeft', 0); let currentWidth = GM_getValue('backgroundWidth', '100%'); // 恢复为百分比 let currentHeight = GM_getValue('backgroundHeight', '100%'); // 恢复为百分比 let currentFitMode = GM_getValue('backgroundFitMode', 'cover'); // 'cover', 'contain', 'repeat' let controlsVisible = false; // 创建控制面板 function createControls() { const controls = document.createElement('div'); controls.id = 'custom-background-controls'; document.body.appendChild(controls); // 文件上传输入 const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; const fileLabel = document.createElement('label'); fileLabel.textContent = '上传图片:'; fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(event) { currentImageUrl = event.target.result; GM_setValue('backgroundImageUrl', currentImageUrl); updateBackground(); controls.querySelector('.preview').src = currentImageUrl; log(`新图片加载: ${currentImageUrl.substring(0, 50)}...`); }; reader.readAsDataURL(file); } }); controls.appendChild(fileLabel); controls.appendChild(fileInput); // 透明度滑块 (0-100) const opacityLabel = document.createElement('label'); opacityLabel.textContent = `透明度:${currentOpacity}%`; const opacityInput = document.createElement('input'); opacityInput.type = 'range'; opacityInput.min = '0'; opacityInput.max = '100'; opacityInput.value = currentOpacity; opacityInput.addEventListener('input', function(e) { currentOpacity = parseInt(e.target.value); opacityLabel.textContent = `透明度:${currentOpacity}%`; GM_setValue('backgroundOpacity', currentOpacity); updateBackground(); }); controls.appendChild(opacityLabel); controls.appendChild(opacityInput); // 位置调整 const topLabel = document.createElement('label'); topLabel.textContent = `顶部位置:${currentTop}px`; const topInput = document.createElement('input'); topInput.type = 'range'; topInput.min = '-500'; topInput.max = '500'; topInput.value = currentTop; topInput.addEventListener('input', function(e) { currentTop = parseInt(e.target.value); topLabel.textContent = `顶部位置:${currentTop}px`; GM_setValue('backgroundTop', currentTop); updateBackground(); }); controls.appendChild(topLabel); controls.appendChild(topInput); const leftLabel = document.createElement('label'); leftLabel.textContent = `左侧位置:${currentLeft}px`; const leftInput = document.createElement('input'); leftInput.type = 'range'; leftInput.min = '-500'; leftInput.max = '500'; leftInput.value = currentLeft; leftInput.addEventListener('input', function(e) { currentLeft = parseInt(e.target.value); leftLabel.textContent = `左侧位置:${currentLeft}px`; GM_setValue('backgroundLeft', currentLeft); updateBackground(); }); controls.appendChild(leftLabel); controls.appendChild(leftInput); // 大小调整 (进度条,转换为像素) const widthLabel = document.createElement('label'); widthLabel.textContent = `宽度:${parseInt(currentWidth) || 1000}px`; const widthInput = document.createElement('input'); widthInput.type = 'range'; widthInput.min = '0'; widthInput.max = '2000'; widthInput.value = parseInt(currentWidth) || 1000; widthInput.addEventListener('input', function(e) { currentWidth = `${parseInt(e.target.value)}px`; widthLabel.textContent = `宽度:${parseInt(e.target.value)}px`; GM_setValue('backgroundWidth', currentWidth); updateBackground(); }); controls.appendChild(widthLabel); controls.appendChild(widthInput); const heightLabel = document.createElement('label'); heightLabel.textContent = `高度:${parseInt(currentHeight) || 1000}px`; const heightInput = document.createElement('input'); heightInput.type = 'range'; heightInput.min = '0'; heightInput.max = '2000'; heightInput.value = parseInt(currentHeight) || 1000; heightInput.addEventListener('input', function(e) { currentHeight = `${parseInt(e.target.value)}px`; heightLabel.textContent = `高度:${parseInt(e.target.value)}px`; GM_setValue('backgroundHeight', currentHeight); updateBackground(); }); controls.appendChild(heightLabel); controls.appendChild(heightInput); // 适应模式选择 const fitLabel = document.createElement('label'); fitLabel.textContent = '适应模式:'; const fitSelect = document.createElement('select'); const fitOptions = [ { value: 'cover', text: '拉伸铺满' }, { value: 'contain', text: '按比例缩放' }, { value: 'repeat', text: '平铺' } ]; fitOptions.forEach(option => { const opt = document.createElement('option'); opt.value = option.value; opt.textContent = option.text; if (option.value === currentFitMode) opt.selected = true; fitSelect.appendChild(opt); }); fitSelect.addEventListener('change', function(e) { currentFitMode = e.target.value; GM_setValue('backgroundFitMode', currentFitMode); updateBackground(); }); controls.appendChild(fitLabel); controls.appendChild(fitSelect); // 预览 const preview = document.createElement('img'); preview.className = 'preview'; preview.src = currentImageUrl; controls.appendChild(preview); // 添加提示 const tip = document.createElement('p'); tip.textContent = '注意:使用 TradingView 相机图标保存图片时,自定义背景可能不会包含。请尝试直接粘贴图片到图表,或使用图片编辑软件合并。'; tip.style.color = '#ffcc00'; tip.style.fontSize = '12px'; controls.appendChild(tip); log('控件创建完成'); } // 更新背景 function updateBackground() { const chartContainer = document.querySelector('.chart-container'); if (!chartContainer) { log('图表容器未找到'); return; } // 移除旧背景 const oldBackground = chartContainer.querySelector('div[background-added]'); if (oldBackground) { oldBackground.remove(); } // 创建新背景 const backgroundDiv = document.createElement('div'); backgroundDiv.setAttribute('background-added', 'true'); backgroundDiv.style.position = 'absolute'; backgroundDiv.style.top = `${currentTop}px`; backgroundDiv.style.left = `${currentLeft}px`; backgroundDiv.style.width = currentWidth; backgroundDiv.style.height = currentHeight; backgroundDiv.style.backgroundImage = `url('${currentImageUrl}')`; backgroundDiv.style.backgroundSize = currentFitMode; backgroundDiv.style.backgroundPosition = 'center'; backgroundDiv.style.backgroundRepeat = currentFitMode === 'repeat' ? 'repeat' : 'no-repeat'; backgroundDiv.style.opacity = currentOpacity / 100; // 转换为 0-1 backgroundDiv.style.zIndex = '1'; // 置于底层 backgroundDiv.style.pointerEvents = 'none'; // 不干扰交互 // 确保 chart-container 支持子元素层级 chartContainer.style.position = 'relative'; chartContainer.style.overflow = 'visible'; // 防止内容被裁剪 chartContainer.style.minWidth = '100%'; // 确保宽度 chartContainer.style.minHeight = '100%'; // 确保高度 chartContainer.insertBefore(backgroundDiv, chartContainer.firstChild); // 调试:检查背景样式 log(`背景样式 - URL: ${currentImageUrl.substring(0, 50)}..., Width: ${currentWidth}, Height: ${currentHeight}`); // 确保 canvas 层级 const canvas = chartContainer.querySelector('canvas'); if (canvas) { canvas.style.position = 'relative'; canvas.style.zIndex = '2'; // 确保 K 线在背景之上 log('K 线图 z-index 设置为 2'); } // 确保 chart-page 层级 const chartPage = document.querySelector('.chart-page'); if (chartPage) { chartPage.style.position = 'relative'; chartPage.style.zIndex = '2'; log('图表页面 z-index 设置为 2'); } // 尝试修复价格刻度(左侧 y-axis) const yAxis = document.querySelector('.y-axis') || document.querySelector('.y-axis-labels'); if (yAxis) { yAxis.style.position = 'relative'; yAxis.style.zIndex = '3'; // 确保价格刻度在最上层 log('价格刻度 z-index 设置为 3'); } log('背景已更新,应用新设置'); } // 切换控制面板显示 function toggleControls() { const controls = document.getElementById('custom-background-controls'); if (controls) { controlsVisible = !controlsVisible; controls.style.display = controlsVisible ? 'block' : 'none'; log(`控件 ${controlsVisible ? '显示' : '隐藏'}`); } } // 初始化 log('脚本启动'); createControls(); updateBackground(); // 注册菜单命令 GM_registerMenuCommand('切换背景控件', toggleControls); // 监控 DOM 变化 const observer = new MutationObserver(() => { const chartContainer = document.querySelector('.chart-container'); if (chartContainer && !chartContainer.querySelector('div[background-added]')) { log('检测到图表容器,更新背景'); updateBackground(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 清理 window.addEventListener('unload', () => observer.disconnect()); })();