feat(web): K线图库对比 —— TradingView Lightweight Charts 实现 + 双库切换

- 按确认方案「两套实现并存对比」:
  - 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 通过
This commit is contained in:
Simon
2026-09-09 07:21:07 +08:00
parent 37510c1b89
commit 03fb463216
7 changed files with 218 additions and 21 deletions
@@ -8,7 +8,8 @@
* - signals(未成交意图):空心标记 —— signal_buy 红圈 ▲ / signal_sell 绿圈
* 组件只做展示,绝不自行计算(数据来自后端 Chart API)。
*
* 基于 ECharts candlestick;组件层隔离,便于后续替换 TradingView Lightweight Charts。
* 基于 ECharts candlestick(ECharts 实现);另一套实现见 CandleChartLW.tsx(TradingView
* Lightweight Charts),两套共用同一 props 接口(chartTypes.ts)供对比。
*/
import { useEffect, useRef } from "react";
import * as echarts from "echarts/core";
@@ -16,6 +17,8 @@ import { BarChart, CandlestickChart, LineChart, ScatterChart } from "echarts/cha
import { GridComponent, TooltipComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import type { ChartMarker } from "@/lib/types";
import type { CandleChartProps } from "./chartTypes";
export type { CandleDatum } from "./chartTypes";
echarts.use([
CandlestickChart,
@@ -38,31 +41,15 @@ const C = {
ma60: "#d4a72c",
};
export interface CandleDatum {
time: string;
open: number;
close: number;
low: number;
high: number;
}
export function CandleChart({
data,
volume,
volume = [],
indicators = {},
fills = [],
signals = [],
height = 480,
name = "",
}: {
data: CandleDatum[];
volume?: { time: string; value: number | null }[];
indicators?: Record<string, { time: string; value: number | null }[]>;
fills?: ChartMarker[];
signals?: ChartMarker[];
height?: number;
name?: string;
}) {
}: CandleChartProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -0,0 +1,154 @@
"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)`} />;
}
@@ -0,0 +1,12 @@
"use client";
/** K 线图统一入口:可切换 ECharts / TradingView Lightweight Charts 两套实现(对比期)。 */
import type { CandleChartProps } from "./chartTypes";
import { CandleChart } from "./CandleChart";
import { CandleChartLW } from "./CandleChartLW";
export type ChartLibrary = "echarts" | "lightweight";
export function StockChart({ library = "lightweight", ...props }: CandleChartProps & { library?: ChartLibrary }) {
return library === "echarts" ? <CandleChart {...props} /> : <CandleChartLW {...props} />;
}
@@ -0,0 +1,20 @@
/** K 线组件共享接口(两套实现共用:ECharts 版与 TradingView Lightweight 版)。 */
import type { ChartMarker, ChartSeriesPoint, ChartVolumePoint } from "@/lib/types";
export interface CandleDatum {
time: string;
open: number;
close: number;
low: number;
high: number;
}
export interface CandleChartProps {
data: CandleDatum[];
volume?: ChartVolumePoint[];
indicators?: Record<string, ChartSeriesPoint[]>;
fills?: ChartMarker[];
signals?: ChartMarker[];
height?: number;
name?: string;
}