| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>专业K线图</title>
- <script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
- <style>
- :root {
- --bg: #0b1220;
- --panel: #111a2e;
- --line: #273552;
- --text: #dbe8ff;
- --muted: #8fa4c7;
- --accent: #2ec7ff;
- --up: #00c087;
- --down: #f6465d;
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- padding: 16px;
- min-height: 100vh;
- color: var(--text);
- background: radial-gradient(circle at 15% 10%, #1a2740 0%, #0f172a 45%, #0b1220 100%);
- font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
- }
- .wrap { max-width: 1320px; margin: 0 auto; display: grid; gap: 12px; }
- .toolbar {
- background: rgba(17, 26, 46, 0.92);
- border: 1px solid var(--line);
- border-radius: 12px;
- padding: 12px;
- display: grid;
- gap: 10px;
- grid-template-columns: 1fr auto auto;
- align-items: center;
- }
- .left { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
- .intervals { display: flex; flex-wrap: wrap; gap: 8px; }
- input, select, button {
- height: 36px;
- border: 1px solid var(--line);
- border-radius: 9px;
- background: #0d1628;
- color: var(--text);
- padding: 0 12px;
- font-size: 14px;
- outline: none;
- }
- button { cursor: pointer; font-weight: 600; }
- .primary {
- background: linear-gradient(135deg, #0ea5e9, #0284c7);
- border-color: #38bdf8;
- color: #e8f8ff;
- }
- .interval-btn.active {
- border-color: var(--accent);
- color: #e9f7ff;
- box-shadow: 0 0 0 1px #1a8fb8 inset;
- }
- .status { color: var(--muted); min-width: 280px; text-align: right; font-size: 13px; }
- .panel {
- background: rgba(17, 26, 46, 0.92);
- border: 1px solid var(--line);
- border-radius: 12px;
- overflow: hidden;
- }
- #chart { width: 100%; height: min(80vh, 800px); }
- @media (max-width: 980px) {
- .toolbar { grid-template-columns: 1fr; }
- .status { text-align: left; min-width: 0; }
- #chart { height: 72vh; }
- }
- </style>
- </head>
- <body>
- <main class="wrap">
- <section class="toolbar">
- <div class="left">
- <input id="symbolInput" value="BTCUSDT" placeholder="币对,如 BTCUSDT" />
- <select id="limitSelect">
- <option value="100">100</option>
- <option value="200" selected>200</option>
- <option value="500">500</option>
- <option value="1000">1000</option>
- </select>
- <button id="refreshBtn" class="primary">刷新</button>
- </div>
- <div class="intervals" id="intervals">
- <button class="interval-btn active" data-v="hour">1小时</button>
- <button class="interval-btn" data-v="day">1天</button>
- <button class="interval-btn" data-v="week">1周</button>
- <button class="interval-btn" data-v="month">1月</button>
- </div>
- <div id="status" class="status">等待加载...</div>
- </section>
- <section class="panel">
- <div id="chart"></div>
- </section>
- </main>
- <script>
- (() => {
- const chartEl = document.getElementById('chart');
- const statusEl = document.getElementById('status');
- const symbolInput = document.getElementById('symbolInput');
- const limitSelect = document.getElementById('limitSelect');
- const refreshBtn = document.getElementById('refreshBtn');
- const intervalsEl = document.getElementById('intervals');
- let currentInterval = 'hour';
- let timer = null;
- const chart = LightweightCharts.createChart(chartEl, {
- autoSize: true,
- layout: {
- background: { color: '#101a30' },
- textColor: '#93a8cc',
- fontFamily: 'Segoe UI, PingFang SC, Microsoft YaHei, sans-serif'
- },
- rightPriceScale: {
- borderColor: '#2a3a58',
- scaleMargins: { top: 0.08, bottom: 0.26 }
- },
- timeScale: {
- borderColor: '#2a3a58',
- rightOffset: 6,
- barSpacing: 10,
- fixLeftEdge: false,
- lockVisibleTimeRangeOnResize: true,
- secondsVisible: false,
- timeVisible: true
- },
- grid: {
- vertLines: { color: 'rgba(78, 97, 130, 0.20)' },
- horzLines: { color: 'rgba(78, 97, 130, 0.20)' }
- },
- crosshair: {
- mode: LightweightCharts.CrosshairMode.Normal,
- vertLine: { color: '#466285', width: 1, style: 3 },
- horzLine: { color: '#466285', width: 1, style: 3 }
- }
- });
- const candleSeries = chart.addCandlestickSeries({
- upColor: '#00c087',
- downColor: '#f6465d',
- borderUpColor: '#00c087',
- borderDownColor: '#f6465d',
- wickUpColor: '#00c087',
- wickDownColor: '#f6465d',
- priceLineVisible: true
- });
- const volumeSeries = chart.addHistogramSeries({
- priceFormat: { type: 'volume' },
- priceScaleId: '',
- color: '#4a90e2',
- base: 0,
- lastValueVisible: false,
- priceLineVisible: false
- });
- volumeSeries.priceScale().applyOptions({
- scaleMargins: { top: 0.80, bottom: 0 }
- });
- const ma5 = chart.addLineSeries({ color: '#f5a524', lineWidth: 1.4, crosshairMarkerVisible: false, priceLineVisible: false });
- const ma10 = chart.addLineSeries({ color: '#4da3ff', lineWidth: 1.2, crosshairMarkerVisible: false, priceLineVisible: false });
- const ma30 = chart.addLineSeries({ color: '#a68bff', lineWidth: 1.2, crosshairMarkerVisible: false, priceLineVisible: false });
- function setStatus(text) { statusEl.textContent = text; }
- function toUnixSeconds(ms) {
- return Math.floor(Number(ms) / 1000);
- }
- function calcMA(day, list) {
- const out = [];
- for (let i = 0; i < list.length; i++) {
- if (i < day - 1) continue;
- let sum = 0;
- for (let j = 0; j < day; j++) sum += Number(list[i - j].close);
- out.push({ time: list[i].time, value: Number((sum / day).toFixed(6)) });
- }
- return out;
- }
- async function loadKline() {
- const symbol = (symbolInput.value || 'BTCUSDT').trim().toUpperCase();
- const limit = limitSelect.value;
- const url = `/admin/coin/kline?symbol=${encodeURIComponent(symbol)}&interval=${encodeURIComponent(currentInterval)}&limit=${encodeURIComponent(limit)}`;
- setStatus('加载中...');
- try {
- const resp = await fetch(url, { cache: 'no-store' });
- const json = await resp.json();
- if (json.code !== 200) {
- setStatus(`加载失败: ${json.data || json.msg || 'unknown error'}`);
- return;
- }
- const items = (json.data && json.data.items) ? json.data.items : [];
- if (!items.length) {
- setStatus('无数据');
- candleSeries.setData([]);
- volumeSeries.setData([]);
- ma5.setData([]); ma10.setData([]); ma30.setData([]);
- return;
- }
- const k = items.map(it => ({
- time: toUnixSeconds(it.open_time),
- open: Number(it.open),
- high: Number(it.high),
- low: Number(it.low),
- close: Number(it.close),
- volume: Number(it.volume)
- }));
- candleSeries.setData(k.map(x => ({
- time: x.time,
- open: x.open,
- high: x.high,
- low: x.low,
- close: x.close
- })));
- volumeSeries.setData(k.map(x => ({
- time: x.time,
- value: x.volume,
- color: x.close >= x.open ? 'rgba(0,192,135,0.58)' : 'rgba(246,70,93,0.58)'
- })));
- ma5.setData(calcMA(5, k));
- ma10.setData(calcMA(10, k));
- ma30.setData(calcMA(30, k));
- chart.timeScale().fitContent();
- setStatus(`${symbol} · ${currentInterval} · ${k.length}根K线 · ${new Date().toLocaleTimeString()}`);
- } catch (e) {
- setStatus(`请求失败: ${e.message}`);
- }
- }
- intervalsEl.addEventListener('click', (e) => {
- const btn = e.target.closest('.interval-btn');
- if (!btn) return;
- document.querySelectorAll('.interval-btn').forEach(x => x.classList.remove('active'));
- btn.classList.add('active');
- currentInterval = btn.dataset.v;
- loadKline();
- });
- refreshBtn.addEventListener('click', loadKline);
- symbolInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') loadKline(); });
- function startAuto() {
- if (timer) clearInterval(timer);
- timer = setInterval(loadKline, 15000);
- }
- startAuto();
- loadKline();
- })();
- </script>
- </body>
- </html>
|