"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 (
);
}
export default function KlinePage() {
const [symbol, setSymbol] = useState<"BTC" | "ETH" | "XRP" | "SOL">("BTC");
const [rateInterval, setRateInterval] = useState("1h");
const [priceInterval, setPriceInterval] = useState("1h");
return (
{/* 标题+币种 */}
费率 K 线
{(["BTC", "ETH", "XRP", "SOL"] as const).map(s => (
))}
{/* 费率K线 */}
{symbol} 资金费率(万分之)
原始值×10000 · 绿涨红跌 · 代表多头情绪
{/* 价格K线 */}
{symbol} 标记价格(USD)
蓝涨紫跌 · 与费率对比观察趋势
说明:
数据来自本地rate_snapshots表(每2秒一条),后端自动采集,永久保留。每30秒刷新。
);
}