"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { createChart, ColorType, CandlestickSeries } from "lightweight-charts"; const INTERVALS = [ { label: "1m", value: "1m" }, { label: "5m", value: "5m" }, { label: "30m", value: "30m" }, { label: "1h", value: "1h" }, { label: "4h", value: "4h" }, { label: "8h", value: "8h" }, { label: "日", value: "1d" }, { label: "周", value: "1w" }, { label: "月", value: "1M" }, ]; interface KBar { time: number; open: number; high: number; low: number; close: number; price_open: number; price_high: number; price_low: number; price_close: number; } function baseChartOptions(height: number) { return { layout: { background: { type: ColorType.Solid, color: "#ffffff" }, textColor: "#64748b", fontSize: 11, }, grid: { vertLines: { color: "#f1f5f9" }, horzLines: { color: "#f1f5f9" } }, localization: { timeFormatter: (ts: number) => { const d = new Date((ts + 8 * 3600) * 1000); const Y = d.getUTCFullYear(); const M = String(d.getUTCMonth() + 1).padStart(2, "0"); const D = String(d.getUTCDate()).padStart(2, "0"); const h = String(d.getUTCHours()).padStart(2, "0"); const m = String(d.getUTCMinutes()).padStart(2, "0"); return `${Y}-${M}-${D} ${h}:${m}`; }, }, timeScale: { borderColor: "#e2e8f0", timeVisible: true, secondsVisible: false, tickMarkFormatter: (ts: number) => { const d = new Date((ts + 8 * 3600) * 1000); return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`; }, }, rightPriceScale: { borderColor: "#e2e8f0" }, height, }; } function IntervalBar({ value, onChange }: { value: string; onChange: (v: string) => void }) { return (
{INTERVALS.map(iv => ( ))}
); } function KChart({ symbol, interval, mode, color, }: { symbol: string; interval: string; mode: "rate" | "price"; color: { up: string; down: string }; }) { const containerRef = useRef(null); const chartRef = useRef | null>(null); const [count, setCount] = useState(0); const render = useCallback(async () => { try { const r = await fetch(`/api/kline?symbol=${symbol}&interval=${interval}&limit=500`); const json = await r.json(); const bars: KBar[] = json.data || []; setCount(json.count || 0); if (!containerRef.current) return; chartRef.current?.remove(); const chart = createChart(containerRef.current, baseChartOptions(260)); chartRef.current = chart; const series = chart.addSeries(CandlestickSeries, { upColor: color.up, downColor: color.down, borderUpColor: color.up, borderDownColor: color.down, wickUpColor: color.up, wickDownColor: color.down, }); if (mode === "rate") { series.setData(bars.map(b => ({ time: b.time as any, open: b.open, high: b.high, low: b.low, close: b.close }))); } else { series.setData(bars.map(b => ({ time: b.time as any, open: b.price_open, high: b.price_high, low: b.price_low, close: b.price_close }))); } chart.timeScale().fitContent(); containerRef.current.querySelectorAll("a").forEach(a => (a as HTMLElement).style.display = "none"); } catch (e) { console.error(e); } }, [symbol, interval, mode, color.up, color.down]); useEffect(() => { render(); const iv = window.setInterval(render, 30_000); return () => { window.clearInterval(iv); chartRef.current?.remove(); chartRef.current = null; }; }, [render]); return (

{count} 根K线

); } export default function KlinePage() { const [symbol, setSymbol] = useState<"BTC" | "ETH">("BTC"); const [rateInterval, setRateInterval] = useState("1h"); const [priceInterval, setPriceInterval] = useState("1h"); return (
{/* 标题+币种 */}

费率 K 线

{(["BTC", "ETH"] as const).map(s => ( ))}
{/* 费率K线 */}

{symbol} 资金费率(万分之)

原始值×10000 · 绿涨红跌 · 代表多头情绪

{/* 价格K线 */}

{symbol} 标记价格(USD)

蓝涨紫跌 · 与费率对比观察趋势

说明: 数据来自本地rate_snapshots表(每2秒一条),后端自动采集,永久保留。每30秒刷新。
); }