kline.tpl 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>专业K线图</title>
  7. <script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
  8. <style>
  9. :root {
  10. --bg: #0b1220;
  11. --panel: #111a2e;
  12. --line: #273552;
  13. --text: #dbe8ff;
  14. --muted: #8fa4c7;
  15. --accent: #2ec7ff;
  16. --up: #00c087;
  17. --down: #f6465d;
  18. }
  19. * { box-sizing: border-box; }
  20. body {
  21. margin: 0;
  22. padding: 16px;
  23. min-height: 100vh;
  24. color: var(--text);
  25. background: radial-gradient(circle at 15% 10%, #1a2740 0%, #0f172a 45%, #0b1220 100%);
  26. font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  27. }
  28. .wrap { max-width: 1320px; margin: 0 auto; display: grid; gap: 12px; }
  29. .toolbar {
  30. background: rgba(17, 26, 46, 0.92);
  31. border: 1px solid var(--line);
  32. border-radius: 12px;
  33. padding: 12px;
  34. display: grid;
  35. gap: 10px;
  36. grid-template-columns: 1fr auto auto;
  37. align-items: center;
  38. }
  39. .left { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  40. .intervals { display: flex; flex-wrap: wrap; gap: 8px; }
  41. input, select, button {
  42. height: 36px;
  43. border: 1px solid var(--line);
  44. border-radius: 9px;
  45. background: #0d1628;
  46. color: var(--text);
  47. padding: 0 12px;
  48. font-size: 14px;
  49. outline: none;
  50. }
  51. button { cursor: pointer; font-weight: 600; }
  52. .primary {
  53. background: linear-gradient(135deg, #0ea5e9, #0284c7);
  54. border-color: #38bdf8;
  55. color: #e8f8ff;
  56. }
  57. .interval-btn.active {
  58. border-color: var(--accent);
  59. color: #e9f7ff;
  60. box-shadow: 0 0 0 1px #1a8fb8 inset;
  61. }
  62. .status { color: var(--muted); min-width: 280px; text-align: right; font-size: 13px; }
  63. .panel {
  64. background: rgba(17, 26, 46, 0.92);
  65. border: 1px solid var(--line);
  66. border-radius: 12px;
  67. overflow: hidden;
  68. }
  69. #chart { width: 100%; height: min(80vh, 800px); }
  70. @media (max-width: 980px) {
  71. .toolbar { grid-template-columns: 1fr; }
  72. .status { text-align: left; min-width: 0; }
  73. #chart { height: 72vh; }
  74. }
  75. </style>
  76. </head>
  77. <body>
  78. <main class="wrap">
  79. <section class="toolbar">
  80. <div class="left">
  81. <input id="symbolInput" value="BTCUSDT" placeholder="币对,如 BTCUSDT" />
  82. <select id="limitSelect">
  83. <option value="100">100</option>
  84. <option value="200" selected>200</option>
  85. <option value="500">500</option>
  86. <option value="1000">1000</option>
  87. </select>
  88. <button id="refreshBtn" class="primary">刷新</button>
  89. </div>
  90. <div class="intervals" id="intervals">
  91. <button class="interval-btn active" data-v="hour">1小时</button>
  92. <button class="interval-btn" data-v="day">1天</button>
  93. <button class="interval-btn" data-v="week">1周</button>
  94. <button class="interval-btn" data-v="month">1月</button>
  95. </div>
  96. <div id="status" class="status">等待加载...</div>
  97. </section>
  98. <section class="panel">
  99. <div id="chart"></div>
  100. </section>
  101. </main>
  102. <script>
  103. (() => {
  104. const chartEl = document.getElementById('chart');
  105. const statusEl = document.getElementById('status');
  106. const symbolInput = document.getElementById('symbolInput');
  107. const limitSelect = document.getElementById('limitSelect');
  108. const refreshBtn = document.getElementById('refreshBtn');
  109. const intervalsEl = document.getElementById('intervals');
  110. let currentInterval = 'hour';
  111. let timer = null;
  112. const chart = LightweightCharts.createChart(chartEl, {
  113. autoSize: true,
  114. layout: {
  115. background: { color: '#101a30' },
  116. textColor: '#93a8cc',
  117. fontFamily: 'Segoe UI, PingFang SC, Microsoft YaHei, sans-serif'
  118. },
  119. rightPriceScale: {
  120. borderColor: '#2a3a58',
  121. scaleMargins: { top: 0.08, bottom: 0.26 }
  122. },
  123. timeScale: {
  124. borderColor: '#2a3a58',
  125. rightOffset: 6,
  126. barSpacing: 10,
  127. fixLeftEdge: false,
  128. lockVisibleTimeRangeOnResize: true,
  129. secondsVisible: false,
  130. timeVisible: true
  131. },
  132. grid: {
  133. vertLines: { color: 'rgba(78, 97, 130, 0.20)' },
  134. horzLines: { color: 'rgba(78, 97, 130, 0.20)' }
  135. },
  136. crosshair: {
  137. mode: LightweightCharts.CrosshairMode.Normal,
  138. vertLine: { color: '#466285', width: 1, style: 3 },
  139. horzLine: { color: '#466285', width: 1, style: 3 }
  140. }
  141. });
  142. const candleSeries = chart.addCandlestickSeries({
  143. upColor: '#00c087',
  144. downColor: '#f6465d',
  145. borderUpColor: '#00c087',
  146. borderDownColor: '#f6465d',
  147. wickUpColor: '#00c087',
  148. wickDownColor: '#f6465d',
  149. priceLineVisible: true
  150. });
  151. const volumeSeries = chart.addHistogramSeries({
  152. priceFormat: { type: 'volume' },
  153. priceScaleId: '',
  154. color: '#4a90e2',
  155. base: 0,
  156. lastValueVisible: false,
  157. priceLineVisible: false
  158. });
  159. volumeSeries.priceScale().applyOptions({
  160. scaleMargins: { top: 0.80, bottom: 0 }
  161. });
  162. const ma5 = chart.addLineSeries({ color: '#f5a524', lineWidth: 1.4, crosshairMarkerVisible: false, priceLineVisible: false });
  163. const ma10 = chart.addLineSeries({ color: '#4da3ff', lineWidth: 1.2, crosshairMarkerVisible: false, priceLineVisible: false });
  164. const ma30 = chart.addLineSeries({ color: '#a68bff', lineWidth: 1.2, crosshairMarkerVisible: false, priceLineVisible: false });
  165. function setStatus(text) { statusEl.textContent = text; }
  166. function toUnixSeconds(ms) {
  167. return Math.floor(Number(ms) / 1000);
  168. }
  169. function calcMA(day, list) {
  170. const out = [];
  171. for (let i = 0; i < list.length; i++) {
  172. if (i < day - 1) continue;
  173. let sum = 0;
  174. for (let j = 0; j < day; j++) sum += Number(list[i - j].close);
  175. out.push({ time: list[i].time, value: Number((sum / day).toFixed(6)) });
  176. }
  177. return out;
  178. }
  179. async function loadKline() {
  180. const symbol = (symbolInput.value || 'BTCUSDT').trim().toUpperCase();
  181. const limit = limitSelect.value;
  182. const url = `/admin/coin/kline?symbol=${encodeURIComponent(symbol)}&interval=${encodeURIComponent(currentInterval)}&limit=${encodeURIComponent(limit)}`;
  183. setStatus('加载中...');
  184. try {
  185. const resp = await fetch(url, { cache: 'no-store' });
  186. const json = await resp.json();
  187. if (json.code !== 200) {
  188. setStatus(`加载失败: ${json.data || json.msg || 'unknown error'}`);
  189. return;
  190. }
  191. const items = (json.data && json.data.items) ? json.data.items : [];
  192. if (!items.length) {
  193. setStatus('无数据');
  194. candleSeries.setData([]);
  195. volumeSeries.setData([]);
  196. ma5.setData([]); ma10.setData([]); ma30.setData([]);
  197. return;
  198. }
  199. const k = items.map(it => ({
  200. time: toUnixSeconds(it.open_time),
  201. open: Number(it.open),
  202. high: Number(it.high),
  203. low: Number(it.low),
  204. close: Number(it.close),
  205. volume: Number(it.volume)
  206. }));
  207. candleSeries.setData(k.map(x => ({
  208. time: x.time,
  209. open: x.open,
  210. high: x.high,
  211. low: x.low,
  212. close: x.close
  213. })));
  214. volumeSeries.setData(k.map(x => ({
  215. time: x.time,
  216. value: x.volume,
  217. color: x.close >= x.open ? 'rgba(0,192,135,0.58)' : 'rgba(246,70,93,0.58)'
  218. })));
  219. ma5.setData(calcMA(5, k));
  220. ma10.setData(calcMA(10, k));
  221. ma30.setData(calcMA(30, k));
  222. chart.timeScale().fitContent();
  223. setStatus(`${symbol} · ${currentInterval} · ${k.length}根K线 · ${new Date().toLocaleTimeString()}`);
  224. } catch (e) {
  225. setStatus(`请求失败: ${e.message}`);
  226. }
  227. }
  228. intervalsEl.addEventListener('click', (e) => {
  229. const btn = e.target.closest('.interval-btn');
  230. if (!btn) return;
  231. document.querySelectorAll('.interval-btn').forEach(x => x.classList.remove('active'));
  232. btn.classList.add('active');
  233. currentInterval = btn.dataset.v;
  234. loadKline();
  235. });
  236. refreshBtn.addEventListener('click', loadKline);
  237. symbolInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') loadKline(); });
  238. function startAuto() {
  239. if (timer) clearInterval(timer);
  240. timer = setInterval(loadKline, 15000);
  241. }
  242. startAuto();
  243. loadKline();
  244. })();
  245. </script>
  246. </body>
  247. </html>