feat: 详情页K线改用 TradingView Lightweight Charts,蜡烛图原生支持
- 新增 KLineChart 组件(lightweight-charts v5):原生蜡烛图/折线切换、成交量副图(涨红跌绿)、自选日标记、双指缩放拖动 - 保留时间范围切换(3月/6月/1年,默认3月,一次性拉取365天前端切片) - 修复 lightweight-charts 无法解析 CSS 变量/oklch 颜色的问题(改用固定 hex) - 替换原 recharts K线(recharts Bar 自定义 shape 无法渲染蜡烛的问题彻底解决)
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"lightweight-charts": "^5.2.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
|
|||||||
Generated
+15
@@ -125,6 +125,9 @@ importers:
|
|||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
lightweight-charts:
|
||||||
|
specifier: ^5.2.1
|
||||||
|
version: 5.2.1
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.575.0
|
specifier: ^0.575.0
|
||||||
version: 0.575.0(react@19.2.7)
|
version: 0.575.0(react@19.2.7)
|
||||||
@@ -1616,6 +1619,9 @@ packages:
|
|||||||
eventemitter3@4.0.1:
|
eventemitter3@4.0.1:
|
||||||
resolution: {integrity: sha512-MnI0l35oYL2C/c80rjJN7qu50MDx39yYE7y7oYck2YA3v+y7EaAenY8IU8AP4d1RWqE8VAKWFGSh3rfP87ll3g==}
|
resolution: {integrity: sha512-MnI0l35oYL2C/c80rjJN7qu50MDx39yYE7y7oYck2YA3v+y7EaAenY8IU8AP4d1RWqE8VAKWFGSh3rfP87ll3g==}
|
||||||
|
|
||||||
|
fancy-canvas@2.1.0:
|
||||||
|
resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==}
|
||||||
|
|
||||||
fast-equals@5.4.1:
|
fast-equals@5.4.1:
|
||||||
resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==}
|
resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -1767,6 +1773,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lightweight-charts@5.2.1:
|
||||||
|
resolution: {integrity: sha512-IVwoK1RLFiLPubaKIjNbtjWLnpPMqiABSrTay6whmNa8L1+19292VtHJ+BWyPUuLCwF0tcQlhEWd1CLB2a1nsQ==}
|
||||||
|
|
||||||
lodash@4.18.1:
|
lodash@4.18.1:
|
||||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
|
||||||
@@ -3498,6 +3507,8 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@4.0.1: {}
|
eventemitter3@4.0.1: {}
|
||||||
|
|
||||||
|
fancy-canvas@2.1.0: {}
|
||||||
|
|
||||||
fast-equals@5.4.1: {}
|
fast-equals@5.4.1: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.5):
|
fdir@6.5.0(picomatch@4.0.5):
|
||||||
@@ -3590,6 +3601,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.32.0
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
lightningcss-win32-x64-msvc: 1.32.0
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
lightweight-charts@5.2.1:
|
||||||
|
dependencies:
|
||||||
|
fancy-canvas: 2.1.0
|
||||||
|
|
||||||
lodash@4.18.1: {}
|
lodash@4.18.1: {}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* K 线图组件(基于 TradingView Lightweight Charts v5)
|
||||||
|
* 支持折线/蜡烛切换、成交量副图、自选日标记。
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import {
|
||||||
|
createChart,
|
||||||
|
CandlestickSeries,
|
||||||
|
LineSeries,
|
||||||
|
HistogramSeries,
|
||||||
|
ColorType,
|
||||||
|
CrosshairMode,
|
||||||
|
LineStyle,
|
||||||
|
createSeriesMarkers,
|
||||||
|
type IChartApi,
|
||||||
|
type ISeriesApi,
|
||||||
|
type Time,
|
||||||
|
} from "lightweight-charts";
|
||||||
|
|
||||||
|
export interface KLineItem {
|
||||||
|
time: string; // ISO 日期 yyyy-mm-dd(lightweight-charts 必需)
|
||||||
|
open: number;
|
||||||
|
close: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
volume: number;
|
||||||
|
isAddedDate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: KLineItem[];
|
||||||
|
mode: "line" | "candle";
|
||||||
|
hasAddedDate?: boolean; // 是否在自选集合中(决定是否显示标记)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightweight-charts 无法解析 CSS 变量或 oklch() 颜色,直接用具体 hex 色。
|
||||||
|
// 涨红跌绿 + 中性灰边框,亮色模式为主(暗色下亦可读)。
|
||||||
|
const UP = "#ef4444";
|
||||||
|
const DOWN = "#22c55e";
|
||||||
|
const PRIMARY = "#ef4444";
|
||||||
|
const TEXT = "#71717a"; // muted-foreground 近似灰
|
||||||
|
const GRID = "#e4e4e7"; // border 近似灰
|
||||||
|
const MARKER = "#f59e0b"; // 自选日标记
|
||||||
|
|
||||||
|
export function KLineChart({ data, mode, hasAddedDate }: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
|
const priceSeriesRef = useRef<ISeriesApi<"Candlestick" | "Line"> | null>(null);
|
||||||
|
const volSeriesRef = useRef<ISeriesApi<"Histogram"> | null>(null);
|
||||||
|
|
||||||
|
// 创建图表(仅一次)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const chart = createChart(containerRef.current, {
|
||||||
|
autoSize: true,
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: "transparent" },
|
||||||
|
textColor: TEXT,
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: GRID, style: LineStyle.Dashed, visible: true },
|
||||||
|
horzLines: { color: GRID, style: LineStyle.Dashed, visible: true },
|
||||||
|
},
|
||||||
|
rightPriceScale: { borderColor: GRID },
|
||||||
|
timeScale: { borderColor: GRID, timeVisible: false },
|
||||||
|
crosshair: { mode: CrosshairMode.Normal },
|
||||||
|
});
|
||||||
|
chartRef.current = chart;
|
||||||
|
|
||||||
|
// 成交量副图(pane 1)
|
||||||
|
const vol = chart.addSeries(HistogramSeries, {
|
||||||
|
priceFormat: { type: "volume" },
|
||||||
|
priceScaleId: "vol",
|
||||||
|
});
|
||||||
|
vol.priceScale().applyOptions({
|
||||||
|
scaleMargins: { top: 0.8, bottom: 0 },
|
||||||
|
});
|
||||||
|
volSeriesRef.current = vol;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chart.remove();
|
||||||
|
chartRef.current = null;
|
||||||
|
priceSeriesRef.current = null;
|
||||||
|
volSeriesRef.current = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 模式切换:重建价格 series
|
||||||
|
useEffect(() => {
|
||||||
|
const chart = chartRef.current;
|
||||||
|
if (!chart) return;
|
||||||
|
// 移除旧的价格 series
|
||||||
|
if (priceSeriesRef.current) {
|
||||||
|
chart.removeSeries(priceSeriesRef.current);
|
||||||
|
priceSeriesRef.current = null;
|
||||||
|
}
|
||||||
|
if (mode === "candle") {
|
||||||
|
priceSeriesRef.current = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: UP,
|
||||||
|
downColor: DOWN,
|
||||||
|
borderUpColor: UP,
|
||||||
|
borderDownColor: DOWN,
|
||||||
|
wickUpColor: UP,
|
||||||
|
wickDownColor: DOWN,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
priceSeriesRef.current = chart.addSeries(LineSeries, {
|
||||||
|
color: PRIMARY,
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 数据变更后重新填充
|
||||||
|
fillData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
// 数据更新
|
||||||
|
useEffect(() => {
|
||||||
|
fillData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data, hasAddedDate]);
|
||||||
|
|
||||||
|
function fillData() {
|
||||||
|
const chart = chartRef.current;
|
||||||
|
const ps = priceSeriesRef.current;
|
||||||
|
const vs = volSeriesRef.current;
|
||||||
|
if (!chart || !ps || !vs) return;
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
ps.setData([]);
|
||||||
|
vs.setData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 蜡烛/折线数据
|
||||||
|
if (mode === "candle") {
|
||||||
|
(ps as ISeriesApi<"Candlestick">).setData(
|
||||||
|
data.map((d) => ({
|
||||||
|
time: d.time as Time,
|
||||||
|
open: d.open,
|
||||||
|
high: d.high,
|
||||||
|
low: d.low,
|
||||||
|
close: d.close,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(ps as ISeriesApi<"Line">).setData(
|
||||||
|
data.map((d) => ({ time: d.time as Time, value: d.close })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成交量柱(涨红跌绿,按当日开收)
|
||||||
|
vs.setData(
|
||||||
|
data.map((d) => ({
|
||||||
|
time: d.time as Time,
|
||||||
|
value: d.volume,
|
||||||
|
color: d.close >= d.open ? UP : DOWN,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自选日标记
|
||||||
|
const markerItem = hasAddedDate ? data.find((d) => d.isAddedDate) : undefined;
|
||||||
|
if (markerItem) {
|
||||||
|
createSeriesMarkers(ps, [
|
||||||
|
{
|
||||||
|
time: markerItem.time as Time,
|
||||||
|
position: "aboveBar",
|
||||||
|
color: MARKER,
|
||||||
|
shape: "circle",
|
||||||
|
text: "自选",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
createSeriesMarkers(ps, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自适应可见范围
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="h-[320px] sm:h-[380px] md:h-[440px] w-full" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KLineChart;
|
||||||
+30
-148
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { ArrowLeft, TrendingUp, TrendingDown, Calendar, ExternalLink, Newspaper } from "lucide-react";
|
import { ArrowLeft, TrendingUp, TrendingDown, Calendar, ExternalLink, Newspaper } from "lucide-react";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import KLineChart from "@/components/kline-chart";
|
||||||
import {
|
import {
|
||||||
ComposedChart,
|
ComposedChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -21,8 +22,6 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Bar,
|
Bar,
|
||||||
Cell,
|
Cell,
|
||||||
Scatter,
|
|
||||||
Brush,
|
|
||||||
ReferenceLine,
|
ReferenceLine,
|
||||||
PieChart,
|
PieChart,
|
||||||
Pie,
|
Pie,
|
||||||
@@ -87,6 +86,7 @@ function StockDetail() {
|
|||||||
const [fundFlowPeriod, setFundFlowPeriod] = useState(21);
|
const [fundFlowPeriod, setFundFlowPeriod] = useState(21);
|
||||||
const [dailyTableDays, setDailyTableDays] = useState<number>(7);
|
const [dailyTableDays, setDailyTableDays] = useState<number>(7);
|
||||||
const [chartRange, setChartRange] = useState(90); // 默认近3月
|
const [chartRange, setChartRange] = useState(90); // 默认近3月
|
||||||
|
const [chartMode, setChartMode] = useState<"line" | "candle">("candle"); // 折线/蜡烛,默认蜡烛
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [inCollection, setInCollection] = useState(false);
|
const [inCollection, setInCollection] = useState(false);
|
||||||
@@ -528,6 +528,20 @@ function StockDetail() {
|
|||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
{/* 显示方式切换:折线 / 蜡烛 */}
|
||||||
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
|
{(["line", "candle"] as const).map((m) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setChartMode(m)}
|
||||||
|
className={`px-2.5 py-1 transition-colors ${
|
||||||
|
chartMode === m ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m === "line" ? "折线" : "蜡烛"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{/* 时间范围:3月 / 6月 / 1年 */}
|
{/* 时间范围:3月 / 6月 / 1年 */}
|
||||||
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
{[
|
{[
|
||||||
@@ -547,7 +561,7 @@ function StockDetail() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground hidden sm:inline">
|
<span className="text-xs text-muted-foreground hidden sm:inline">
|
||||||
双指缩放 · 拖动选区查看
|
双指缩放 · 拖动查看
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,151 +576,19 @@ function StockDetail() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<KLineChart
|
||||||
className="h-[320px] sm:h-[380px] md:h-[440px] w-full touch-none select-none"
|
data={displayData.map((d) => ({
|
||||||
style={{ touchAction: "none", overscrollBehavior: "none", WebkitOverflowScrolling: "auto" }}
|
time: d.dateObj.toISOString().slice(0, 10),
|
||||||
>
|
open: d.open,
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
close: d.close,
|
||||||
<ComposedChart
|
high: d.high,
|
||||||
data={displayData}
|
low: d.low,
|
||||||
margin={{ top: 10, right: 8, left: 0, bottom: 0 }}
|
volume: d.volume,
|
||||||
>
|
isAddedDate: d.isAddedDate,
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" opacity={0.5} />
|
}))}
|
||||||
<XAxis
|
mode={chartMode}
|
||||||
dataKey="date"
|
hasAddedDate={inCollection}
|
||||||
stroke="var(--muted-foreground)"
|
/>
|
||||||
fontSize={10}
|
|
||||||
ticks={tickDates}
|
|
||||||
angle={-45}
|
|
||||||
textAnchor="end"
|
|
||||||
height={56}
|
|
||||||
tick={(props: any) => {
|
|
||||||
const { x, y, payload } = props;
|
|
||||||
const dataPoint = displayData.find(d => d.date === payload.value);
|
|
||||||
const isAddedDate = inCollection && dataPoint?.isAddedDate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
dy={16}
|
|
||||||
textAnchor="end"
|
|
||||||
fill={isAddedDate ? '#f59e0b' : 'var(--muted-foreground)'}
|
|
||||||
fontWeight={isAddedDate ? 'bold' : 'normal'}
|
|
||||||
fontSize={isAddedDate ? 11 : 10}
|
|
||||||
>
|
|
||||||
{payload.value}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="left"
|
|
||||||
stroke="var(--muted-foreground)"
|
|
||||||
fontSize={10}
|
|
||||||
domain={["auto", "auto"]}
|
|
||||||
tickCount={5}
|
|
||||||
width={42}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="right"
|
|
||||||
orientation="right"
|
|
||||||
stroke="var(--muted-foreground)"
|
|
||||||
fontSize={10}
|
|
||||||
tickCount={5}
|
|
||||||
width={42}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: "var(--card)",
|
|
||||||
border: "1px solid var(--border)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
padding: "8px 12px",
|
|
||||||
}}
|
|
||||||
labelFormatter={(label) => `日期: ${label}`}
|
|
||||||
formatter={(value: any, name: string) => {
|
|
||||||
if (name === 'close') return [`¥${Number(value).toFixed(2)}`, '收盘价'];
|
|
||||||
if (name === 'volume') return [Number(value).toLocaleString(), '成交量'];
|
|
||||||
return [value, name];
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
yAxisId="right"
|
|
||||||
dataKey="volume"
|
|
||||||
fill="var(--primary)"
|
|
||||||
opacity={0.2}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="close"
|
|
||||||
stroke="var(--primary)"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
{/* 高亮添加日期的K线点 */}
|
|
||||||
<Scatter
|
|
||||||
yAxisId="left"
|
|
||||||
dataKey="close"
|
|
||||||
fill="#fbbf24"
|
|
||||||
shape={(props: any) => {
|
|
||||||
const { cx, cy, payload } = props;
|
|
||||||
if (!payload.isAddedDate) {
|
|
||||||
return <circle cx={cx} cy={cy} r={0} fill="transparent" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
cx={cx}
|
|
||||||
cy={cy}
|
|
||||||
r={7}
|
|
||||||
fill="#fbbf24"
|
|
||||||
stroke="#f59e0b"
|
|
||||||
strokeWidth={2.5}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 添加日期参考线 */}
|
|
||||||
{addedDataPoint && (
|
|
||||||
<ReferenceLine
|
|
||||||
yAxisId="left"
|
|
||||||
x={addedDataPoint.date}
|
|
||||||
stroke="#f59e0b"
|
|
||||||
strokeDasharray="4 4"
|
|
||||||
opacity={0.7}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* 缩放/滑动控件:移动端可双指缩放,拖动选区查看局部 */}
|
|
||||||
<Brush
|
|
||||||
dataKey="date"
|
|
||||||
height={24}
|
|
||||||
stroke="var(--primary)"
|
|
||||||
fill="var(--muted)"
|
|
||||||
travellerWidth={8}
|
|
||||||
gap={8}
|
|
||||||
/>
|
|
||||||
</ComposedChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="flex flex-wrap justify-center gap-3 md:gap-6 mt-2 md:mt-4 text-xs md:text-sm">
|
|
||||||
<div className="flex items-center gap-1.5 md:gap-2">
|
|
||||||
<div className="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-primary"></div>
|
|
||||||
<span>收盘价</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 md:gap-2">
|
|
||||||
<div className="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-primary opacity-20"></div>
|
|
||||||
<span>成交量</span>
|
|
||||||
</div>
|
|
||||||
{inCollection && (
|
|
||||||
<div className="flex items-center gap-1.5 md:gap-2">
|
|
||||||
<div className="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full" style={{ backgroundColor: '#fbbf24' }}></div>
|
|
||||||
<span>自选日期</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user