- 按确认方案「两套实现并存对比」:
- npm 依赖 lightweight-charts@4.2.3(v3 §21 推荐库)
- components/StockChart/chartTypes.ts:共享 props 接口
- CandleChart.tsx(ECharts 版,重构复用共享类型)+ CandleChartLW.tsx
(Lightweight 版:蜡烛+成交量副刻度+MA+成交实心箭头/未成交信号圆点 markers)
- StockChart.tsx wrapper;个股页顶部「ECharts / Lightweight」切换(默认 Lightweight)
- /stocks/600519.SH 冒烟 200;tsc --noEmit 通过
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* K 线图 —— TradingView Lightweight Charts 实现(v3 §21 推荐库)。
|
||
*
|
||
* 与 ECharts 版(CandleChart.tsx)共用同一 props 接口(chartTypes.ts),
|
||
* 供「两套实现并存对比」,组件层可整体替换。
|
||
* 标记语义(v3 §20.3):成交 fills = 实心箭头;未成交信号 = 空心圆点。
|
||
* 组件只展示,不重算。
|
||
*/
|
||
import { useEffect, useRef } from "react";
|
||
import {
|
||
createChart,
|
||
ColorType,
|
||
CrosshairMode,
|
||
type IChartApi,
|
||
type ISeriesApi,
|
||
type SeriesMarker,
|
||
type Time,
|
||
} from "lightweight-charts";
|
||
import type { CandleChartProps } from "./chartTypes";
|
||
|
||
const C = {
|
||
faint: "#8b98b2",
|
||
grid: "rgba(150,165,195,0.09)",
|
||
up: "#e5484d",
|
||
down: "#2fb36b",
|
||
ma20: "#3fb6ff",
|
||
ma60: "#d4a72c",
|
||
text: "#e9eef7",
|
||
markerBuy: "#3ddc97",
|
||
markerSell: "#ff5a5a",
|
||
markerSig: "#b98bff",
|
||
};
|
||
|
||
function toTime(t: string): Time {
|
||
return t as Time; // 'YYYY-MM-DD'(v4 支持字符串 business day)
|
||
}
|
||
|
||
export function CandleChartLW({
|
||
data,
|
||
volume = [],
|
||
indicators = {},
|
||
fills = [],
|
||
signals = [],
|
||
height = 480,
|
||
name = "",
|
||
}: CandleChartProps) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (!ref.current || data.length === 0) return;
|
||
const chart: IChartApi = createChart(ref.current, {
|
||
autoSize: true,
|
||
height,
|
||
layout: {
|
||
background: { type: ColorType.Solid, color: "transparent" },
|
||
textColor: C.text,
|
||
fontSize: 11,
|
||
},
|
||
grid: {
|
||
vertLines: { color: C.grid },
|
||
horzLines: { color: C.grid },
|
||
},
|
||
rightPriceScale: { borderColor: C.grid },
|
||
timeScale: { borderColor: C.grid, timeVisible: false },
|
||
crosshair: { mode: CrosshairMode.Normal },
|
||
});
|
||
|
||
const candle: ISeriesApi<"Candlestick"> = chart.addCandlestickSeries({
|
||
upColor: C.up,
|
||
downColor: C.down,
|
||
borderUpColor: C.up,
|
||
borderDownColor: C.down,
|
||
wickUpColor: C.up,
|
||
wickDownColor: C.down,
|
||
});
|
||
candle.setData(
|
||
data.map((d) => ({
|
||
time: toTime(d.time),
|
||
open: d.open,
|
||
high: d.high,
|
||
low: d.low,
|
||
close: d.close,
|
||
}))
|
||
);
|
||
|
||
// 成交量(独立价格刻度,置于底部 20%)
|
||
const vol: ISeriesApi<"Histogram"> = chart.addHistogramSeries({
|
||
priceFormat: { type: "volume" },
|
||
priceScaleId: "vol",
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
chart.priceScale("vol").applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
|
||
vol.setData(
|
||
volume.map((v) => ({ time: toTime(v.time), value: v.value ?? 0, color: C.grid }))
|
||
);
|
||
|
||
// MA 指标线
|
||
for (const [name, pts] of Object.entries(indicators)) {
|
||
const line = chart.addLineSeries({
|
||
color: name === "ma60" ? C.ma60 : C.ma20,
|
||
lineWidth: 1,
|
||
priceLineVisible: false,
|
||
lastValueVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
line.setData(
|
||
pts
|
||
.filter((p) => p.value != null)
|
||
.map((p) => ({ time: toTime(p.time), value: p.value as number }))
|
||
);
|
||
}
|
||
|
||
// 事件标记:成交=实心箭头;未成交信号=圆点
|
||
const markers: SeriesMarker<Time>[] = [];
|
||
for (const m of fills) {
|
||
const buy = m.kind.endsWith("buy");
|
||
markers.push({
|
||
time: toTime(m.time),
|
||
position: buy ? "belowBar" : "aboveBar",
|
||
color: buy ? C.markerBuy : C.markerSell,
|
||
shape: buy ? "arrowUp" : "arrowDown",
|
||
size: 1,
|
||
text: buy ? "B" : "S",
|
||
});
|
||
}
|
||
for (const m of signals) {
|
||
const buy = m.kind.endsWith("buy");
|
||
markers.push({
|
||
time: toTime(m.time),
|
||
position: buy ? "belowBar" : "aboveBar",
|
||
color: C.markerSig,
|
||
shape: "circle",
|
||
size: 1,
|
||
text: buy ? "sigB" : "sigS",
|
||
});
|
||
}
|
||
if (markers.length) candle.setMarkers(markers);
|
||
if (data.length) chart.timeScale().fitContent();
|
||
|
||
const onResize = () => chart.applyOptions({ width: ref.current?.clientWidth });
|
||
window.addEventListener("resize", onResize);
|
||
return () => {
|
||
window.removeEventListener("resize", onResize);
|
||
chart.remove();
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [data, volume, indicators, fills, signals, height]);
|
||
|
||
if (data.length === 0) return <div className="hint">暂无 K 线数据</div>;
|
||
return <div ref={ref} style={{ height, width: "100%" }} aria-label={`${name} K 线图(Lightweight)`} />;
|
||
}
|