feat: K线成交量独立pane、浅色调、移除KDJ
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* K 线图组件(基于 TradingView Lightweight Charts v5)
|
* K 线图组件(基于 TradingView Lightweight Charts v5)
|
||||||
* 主图:蜡烛/折线 + MA(5/10/20/30);
|
* 主图:蜡烛/折线 + MA(5/10/20/30);
|
||||||
* 副图:成交量(pane1)、MACD(pane2)、RSI(pane3)、KDJ(pane4),可开关。
|
* 副图:成交量(pane1)、MACD(pane2)、RSI(pane3),可开关。
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
type ISeriesApi as ISeriesAny,
|
type ISeriesApi as ISeriesAny,
|
||||||
type Time,
|
type Time,
|
||||||
} from "lightweight-charts";
|
} from "lightweight-charts";
|
||||||
import { sma, macd as calcMacd, rsi as calcRsi, kdj as calcKdj, type KBar } from "@/lib/indicators";
|
import { sma, macd as calcMacd, rsi as calcRsi, type KBar } from "@/lib/indicators";
|
||||||
|
|
||||||
export interface KLineItem {
|
export interface KLineItem {
|
||||||
time: string; // ISO 日期 yyyy-mm-dd(lightweight-charts 必需)
|
time: string; // ISO 日期 yyyy-mm-dd(lightweight-charts 必需)
|
||||||
@@ -34,7 +34,6 @@ export interface IndicatorToggles {
|
|||||||
ma: boolean;
|
ma: boolean;
|
||||||
macd: boolean;
|
macd: boolean;
|
||||||
rsi: boolean;
|
rsi: boolean;
|
||||||
kdj: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -47,6 +46,9 @@ interface Props {
|
|||||||
// lightweight-charts 无法解析 CSS 变量或 oklch() 颜色,直接用具体 hex 色。
|
// lightweight-charts 无法解析 CSS 变量或 oklch() 颜色,直接用具体 hex 色。
|
||||||
const UP = "#ef4444";
|
const UP = "#ef4444";
|
||||||
const DOWN = "#22c55e";
|
const DOWN = "#22c55e";
|
||||||
|
// 成交量柱:浅色调,避免与 K 线柱体视觉争夺
|
||||||
|
const VOL_UP = "#fca5a5";
|
||||||
|
const VOL_DOWN = "#86efac";
|
||||||
const PRIMARY = "#ef4444";
|
const PRIMARY = "#ef4444";
|
||||||
const TEXT = "#71717a";
|
const TEXT = "#71717a";
|
||||||
const GRID = "#e4e4e7";
|
const GRID = "#e4e4e7";
|
||||||
@@ -58,9 +60,6 @@ const MA_PERIODS = [5, 10, 20];
|
|||||||
const MACD_DIF = "#3b82f6";
|
const MACD_DIF = "#3b82f6";
|
||||||
const MACD_DEA = "#f59e0b";
|
const MACD_DEA = "#f59e0b";
|
||||||
const RSI_COLOR = "#a855f7";
|
const RSI_COLOR = "#a855f7";
|
||||||
const KDJ_K = "#3b82f6";
|
|
||||||
const KDJ_D = "#f59e0b";
|
|
||||||
const KDJ_J = "#a855f7";
|
|
||||||
|
|
||||||
export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -71,10 +70,9 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
const maSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
const maSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
||||||
const macdSeriesRef = useRef<ISeriesAny<"Line" | "Histogram">[]>([]);
|
const macdSeriesRef = useRef<ISeriesAny<"Line" | "Histogram">[]>([]);
|
||||||
const rsiSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
const rsiSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
||||||
const kdjSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
|
||||||
|
|
||||||
// 容器高度随指标 pane 数量增长,避免主图被压缩
|
// 容器高度随指标 pane 数量增长,避免主图被压缩
|
||||||
const extraPanes = [indicators.macd, indicators.rsi, indicators.kdj].filter(Boolean).length;
|
const extraPanes = [indicators.macd, indicators.rsi].filter(Boolean).length;
|
||||||
// 固定图表高度:主图+成交量 300,每个指标副图 +95
|
// 固定图表高度:主图+成交量 300,每个指标副图 +95
|
||||||
const chartHeight = 300 + extraPanes * 95;
|
const chartHeight = 300 + extraPanes * 95;
|
||||||
|
|
||||||
@@ -101,11 +99,15 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
});
|
});
|
||||||
chartRef.current = chart;
|
chartRef.current = chart;
|
||||||
|
|
||||||
const vol = chart.addSeries(HistogramSeries, {
|
// 成交量放到独立 pane1,避免与主图K线重叠
|
||||||
|
const vol = chart.addSeries(
|
||||||
|
HistogramSeries,
|
||||||
|
{
|
||||||
priceFormat: { type: "volume" },
|
priceFormat: { type: "volume" },
|
||||||
priceScaleId: "vol",
|
priceScaleId: "vol",
|
||||||
});
|
},
|
||||||
vol.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
|
1,
|
||||||
|
);
|
||||||
volSeriesRef.current = vol;
|
volSeriesRef.current = vol;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -116,7 +118,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
maSeriesRef.current = [];
|
maSeriesRef.current = [];
|
||||||
macdSeriesRef.current = [];
|
macdSeriesRef.current = [];
|
||||||
rsiSeriesRef.current = [];
|
rsiSeriesRef.current = [];
|
||||||
kdjSeriesRef.current = [];
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -222,25 +223,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
);
|
);
|
||||||
rsiSeriesRef.current.push(s);
|
rsiSeriesRef.current.push(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- KDJ(pane4)---
|
|
||||||
removeGroup(kdjSeriesRef.current, chart);
|
|
||||||
if (indicators.kdj && bars.length > 0) {
|
|
||||||
const { k, d, j } = calcKdj(bars);
|
|
||||||
const mk = (color: string) =>
|
|
||||||
chart.addSeries(
|
|
||||||
LineSeries,
|
|
||||||
{ color, lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
|
|
||||||
4,
|
|
||||||
);
|
|
||||||
const kS = mk(KDJ_K);
|
|
||||||
const dS = mk(KDJ_D);
|
|
||||||
const jS = mk(KDJ_J);
|
|
||||||
kS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: k[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
|
||||||
dS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: d[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
|
||||||
jS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: j[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
|
||||||
kdjSeriesRef.current.push(kS, dS, jS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模式切换:重建价格 series + 指标
|
// 模式切换:重建价格 series + 指标
|
||||||
@@ -309,7 +291,7 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
data.map((d) => ({
|
data.map((d) => ({
|
||||||
time: d.time as Time,
|
time: d.time as Time,
|
||||||
value: d.volume,
|
value: d.volume,
|
||||||
color: d.close >= d.open ? UP : DOWN,
|
color: d.close >= d.open ? VOL_UP : VOL_DOWN,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -340,9 +322,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
|||||||
legend.push({ color: MACD_DIF, label: "DIF" }, { color: MACD_DEA, label: "DEA" });
|
legend.push({ color: MACD_DIF, label: "DIF" }, { color: MACD_DEA, label: "DEA" });
|
||||||
}
|
}
|
||||||
if (indicators.rsi) legend.push({ color: RSI_COLOR, label: "RSI14" });
|
if (indicators.rsi) legend.push({ color: RSI_COLOR, label: "RSI14" });
|
||||||
if (indicators.kdj) {
|
|
||||||
legend.push({ color: KDJ_K, label: "K" }, { color: KDJ_D, label: "D" }, { color: KDJ_J, label: "J" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
+96
-15
@@ -1,7 +1,7 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState, useEffect, useMemo, Fragment } from "react";
|
import { useState, useEffect, useMemo, Fragment } from "react";
|
||||||
import { collectionsApi } from "@/lib/api-client";
|
import { collectionsApi } from "@/lib/api-client";
|
||||||
import { fetchStockQuote, fetchStockFundFlow, fetchCompanyProfile, fetchBusinessSegments, fetchFinancialData, getStockBoard, type StockQuote, type KLineData, type FundFlowData, type FundFlowSummary, type CompanyProfile, type BusinessSegmentsResponse, type FinancialDataResponse } from "@/lib/stock-api";
|
import { fetchStockQuote, fetchStockFundFlow, fetchCompanyProfile, fetchBusinessSegments, fetchFinancialData, getStockBoard, type StockQuote, type KLineData, type FundFlowData, type FundFlowSummary, type CompanyProfile, type BusinessSegmentsResponse, type FinancialDataResponse, fetchStockHistoryMinute } from "@/lib/stock-api";
|
||||||
import { fetchStockHistoryV2 } from "@/lib/fuyao-api";
|
import { fetchStockHistoryV2 } from "@/lib/fuyao-api";
|
||||||
import StockProfileTabs from "@/components/stock-profile-tabs";
|
import StockProfileTabs from "@/components/stock-profile-tabs";
|
||||||
import { getUserId } from "@/lib/user-id";
|
import { getUserId } from "@/lib/user-id";
|
||||||
@@ -37,6 +37,15 @@ export const Route = createFileRoute("/stock/$code")({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// K线周期标签映射
|
||||||
|
const PERIOD_LABEL: Record<"m1" | "m5" | "m15" | "m30" | "m60", string> = {
|
||||||
|
m1: "1分",
|
||||||
|
m5: "5分",
|
||||||
|
m15: "15分",
|
||||||
|
m30: "30分",
|
||||||
|
m60: "60分",
|
||||||
|
};
|
||||||
|
|
||||||
interface StockData {
|
interface StockData {
|
||||||
date: string;
|
date: string;
|
||||||
dateObj: Date;
|
dateObj: Date;
|
||||||
@@ -87,7 +96,12 @@ function StockDetail() {
|
|||||||
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 [chartMode, setChartMode] = useState<"line" | "candle">("candle"); // 折线/蜡烛,默认蜡烛
|
||||||
const [indicators, setIndicators] = useState({ ma: true, macd: false, rsi: false, kdj: false });
|
// K线周期:日(d) / 60分(m60) / 30分(m30) / 15分(m15) / 5分(m5) / 1分(m1)
|
||||||
|
const [chartPeriod, setChartPeriod] = useState<"d" | "m60" | "m30" | "m15" | "m5" | "m1">("d");
|
||||||
|
// 分钟/小时线专用数据(独立于日K chartData,避免数据混用)
|
||||||
|
const [minuteData, setMinuteData] = useState<StockData[]>([]);
|
||||||
|
const [minuteLoading, setMinuteLoading] = useState(false);
|
||||||
|
const [indicators, setIndicators] = useState({ ma: true, macd: false, rsi: false });
|
||||||
const toggleIndicator = (key: keyof typeof indicators) =>
|
const toggleIndicator = (key: keyof typeof indicators) =>
|
||||||
setIndicators((prev) => ({ ...prev, [key]: !prev[key] }));
|
setIndicators((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -103,7 +117,7 @@ function StockDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStockData();
|
loadStockData();
|
||||||
}, [code]);
|
}, [code, chartPeriod]);
|
||||||
|
|
||||||
// 加载公司概况、经营分析、财务分析(并行后置加载)
|
// 加载公司概况、经营分析、财务分析(并行后置加载)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -124,7 +138,9 @@ function StockDetail() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setChartData([]);
|
setChartData([]);
|
||||||
|
setMinuteData([]);
|
||||||
setChartLoading(false);
|
setChartLoading(false);
|
||||||
|
setMinuteLoading(false);
|
||||||
setFundFlowData([]);
|
setFundFlowData([]);
|
||||||
setFundFlowSummary(null);
|
setFundFlowSummary(null);
|
||||||
setFundFlowLoading(false);
|
setFundFlowLoading(false);
|
||||||
@@ -177,16 +193,16 @@ function StockDetail() {
|
|||||||
// 基础信息已就绪,结束主loading,先渲染页面框架
|
// 基础信息已就绪,结束主loading,先渲染页面框架
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// 阶段2:并行加载历史K线 + 资金流向(后置加载,不阻塞首屏)
|
// 阶段2:根据周期加载不同的K线数据(后置加载,不阻塞首屏)
|
||||||
const addedDate = new Date(addedAt);
|
const addedDate = new Date(addedAt);
|
||||||
// 一次性拉取最多 1 年(365天)K线,展示范围由 chartRange 前端切片
|
|
||||||
const chartDays = 365;
|
|
||||||
|
|
||||||
|
if (chartPeriod === "d") {
|
||||||
|
// 日线:获取完整数据用于展示切片
|
||||||
setChartLoading(true);
|
setChartLoading(true);
|
||||||
setFundFlowLoading(true);
|
setFundFlowLoading(true);
|
||||||
|
|
||||||
const [historyResult, fundFlowResult] = await Promise.allSettled([
|
const [historyResult, fundFlowResult] = await Promise.allSettled([
|
||||||
fetchStockHistoryV2(code, chartDays),
|
fetchStockHistoryV2(code, 365), // 获取1年数据用于切片
|
||||||
fetchStockFundFlow(code, quote.name, 21),
|
fetchStockFundFlow(code, quote.name, 21),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -217,6 +233,36 @@ function StockDetail() {
|
|||||||
} else {
|
} else {
|
||||||
console.error("获取资金流向数据失败:", fundFlowResult.status === "rejected" ? fundFlowResult.reason : "未知错误");
|
console.error("获取资金流向数据失败:", fundFlowResult.status === "rejected" ? fundFlowResult.reason : "未知错误");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 分钟/小时线:独立加载,不影响日K数据
|
||||||
|
setMinuteLoading(true);
|
||||||
|
const minuteResult = await fetchStockHistoryMinute(
|
||||||
|
code,
|
||||||
|
chartPeriod === "m60" ? "m60" :
|
||||||
|
chartPeriod === "m30" ? "m30" :
|
||||||
|
chartPeriod === "m15" ? "m15" :
|
||||||
|
chartPeriod === "m5" ? "m5" : "m1",
|
||||||
|
320 // 默认320根
|
||||||
|
);
|
||||||
|
|
||||||
|
if (minuteResult && minuteResult.length > 0) {
|
||||||
|
const minuteDataConverted = convertToStockData(minuteResult, addedDate);
|
||||||
|
// 如果添加日不是交易日(周末/节假日),标记最接近的K线
|
||||||
|
if (stockRecord && !minuteDataConverted.some(d => d.isAddedDate)) {
|
||||||
|
const closestIdx = findClosestDateIndex(minuteDataConverted, addedDate);
|
||||||
|
if (minuteDataConverted[closestIdx]) {
|
||||||
|
minuteDataConverted[closestIdx] = { ...minuteDataConverted[closestIdx], isAddedDate: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMinuteData(minuteDataConverted);
|
||||||
|
} else {
|
||||||
|
console.error("无法获取分钟K线数据");
|
||||||
|
setError("获取分钟K线数据失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
setMinuteLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("加载失败:", err);
|
console.error("加载失败:", err);
|
||||||
const msg = err instanceof Error ? err.message : "加载股票数据失败";
|
const msg = err instanceof Error ? err.message : "加载股票数据失败";
|
||||||
@@ -224,6 +270,7 @@ function StockDetail() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setChartLoading(false);
|
setChartLoading(false);
|
||||||
|
setMinuteLoading(false);
|
||||||
setFundFlowLoading(false);
|
setFundFlowLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -292,12 +339,20 @@ function StockDetail() {
|
|||||||
return typeof roe === "number" ? roe : null;
|
return typeof roe === "number" ? roe : null;
|
||||||
}, [financialData]);
|
}, [financialData]);
|
||||||
|
|
||||||
// 展示数据 = 按 chartRange 切片最近 N 天(chartData 存了全年,这里只取窗口)
|
// 展示数据:日线时按 chartRange 切片,分钟/小时线时直接使用 minuteData
|
||||||
// 必须在所有 early return 之前调用,保证 hook 顺序一致
|
// 必须在所有 early return 之前调用,保证 hook 顺序一致
|
||||||
const displayData = useMemo(() => {
|
const displayData = useMemo(() => {
|
||||||
|
if (chartPeriod !== "d") {
|
||||||
|
// 分钟/小时线:直接使用后端返回的数据(已经按时间升序)
|
||||||
|
return minuteData;
|
||||||
|
}
|
||||||
|
// 日线:按 chartRange 切片最近 N 天
|
||||||
if (chartData.length <= chartRange) return chartData;
|
if (chartData.length <= chartRange) return chartData;
|
||||||
return chartData.slice(chartData.length - chartRange);
|
return chartData.slice(chartData.length - chartRange);
|
||||||
}, [chartData, chartRange]);
|
}, [chartData, chartRange, chartPeriod, minuteData]);
|
||||||
|
|
||||||
|
// 当前 loading 状态:日线和分钟线用不同的 loading 标志
|
||||||
|
const currentChartLoading = chartPeriod === "d" ? chartLoading : minuteLoading;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -527,7 +582,9 @@ function StockDetail() {
|
|||||||
<CardTitle className="text-base md:text-lg">
|
<CardTitle className="text-base md:text-lg">
|
||||||
K线图
|
K线图
|
||||||
<span className="text-xs md:text-sm font-normal text-muted-foreground ml-2">
|
<span className="text-xs md:text-sm font-normal text-muted-foreground ml-2">
|
||||||
{chartLoading ? "(加载中...)" : `(${displayData.length}个交易日)`}
|
{currentChartLoading ? "(加载中...)" : (
|
||||||
|
chartPeriod === "d" ? `(${displayData.length}个交易日)` : `(${displayData.length}根${PERIOD_LABEL[chartPeriod as "m60" | "m30" | "m15" | "m5" | "m1"] || chartPeriod}K线)`
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
@@ -545,13 +602,12 @@ function StockDetail() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* 指标开关:MA / MACD / RSI / KDJ */}
|
{/* 指标开关:MA / MACD / RSI */}
|
||||||
<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">
|
||||||
{([
|
{([
|
||||||
{ key: "ma", label: "MA" },
|
{ key: "ma", label: "MA" },
|
||||||
{ key: "macd", label: "MACD" },
|
{ key: "macd", label: "MACD" },
|
||||||
{ key: "rsi", label: "RSI" },
|
{ key: "rsi", label: "RSI" },
|
||||||
{ key: "kdj", label: "KDJ" },
|
|
||||||
] as const).map((it) => (
|
] as const).map((it) => (
|
||||||
<button
|
<button
|
||||||
key={it.key}
|
key={it.key}
|
||||||
@@ -564,7 +620,29 @@ function StockDetail() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* 时间范围:3月 / 6月 / 1年 */}
|
{/* K线周期:日 / 60分 / 30分 / 15分 / 5分 / 1分 */}
|
||||||
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
|
{([
|
||||||
|
{ key: "d" as const, label: "日" },
|
||||||
|
{ key: "m60" as const, label: "60分" },
|
||||||
|
{ key: "m30" as const, label: "30分" },
|
||||||
|
{ key: "m15" as const, label: "15分" },
|
||||||
|
{ key: "m5" as const, label: "5分" },
|
||||||
|
{ key: "m1" as const, label: "1分" },
|
||||||
|
] as const).map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.key}
|
||||||
|
onClick={() => setChartPeriod(p.key)}
|
||||||
|
className={`px-2 py-1 transition-colors ${
|
||||||
|
chartPeriod === p.key ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* 时间范围:3月 / 6月 / 1年(仅日线显示) */}
|
||||||
|
{chartPeriod === "d" && (
|
||||||
<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">
|
||||||
{[
|
{[
|
||||||
{ key: 90, label: "3月" },
|
{ key: 90, label: "3月" },
|
||||||
@@ -582,6 +660,7 @@ function StockDetail() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<span className="text-xs text-muted-foreground hidden sm:inline">
|
<span className="text-xs text-muted-foreground hidden sm:inline">
|
||||||
双指缩放 · 拖动查看
|
双指缩放 · 拖动查看
|
||||||
</span>
|
</span>
|
||||||
@@ -589,7 +668,7 @@ function StockDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 md:px-6 pb-2 md:pb-6">
|
<CardContent className="px-2 md:px-6 pb-2 md:pb-6">
|
||||||
{chartLoading ? (
|
{currentChartLoading ? (
|
||||||
<div className="h-[320px] sm:h-[380px] md:h-[440px] w-full flex items-center justify-center">
|
<div className="h-[320px] sm:h-[380px] md:h-[440px] w-full flex items-center justify-center">
|
||||||
<div className="flex items-center text-muted-foreground text-sm">
|
<div className="flex items-center text-muted-foreground text-sm">
|
||||||
<div className="animate-pulse mr-2 h-2 w-2 rounded-full bg-primary"></div>
|
<div className="animate-pulse mr-2 h-2 w-2 rounded-full bg-primary"></div>
|
||||||
@@ -600,7 +679,9 @@ function StockDetail() {
|
|||||||
<>
|
<>
|
||||||
<KLineChart
|
<KLineChart
|
||||||
data={displayData.map((d) => ({
|
data={displayData.map((d) => ({
|
||||||
time: d.dateObj.toISOString().slice(0, 10),
|
time: chartPeriod === "d"
|
||||||
|
? d.dateObj.toISOString().slice(0, 10)
|
||||||
|
: d.date, // 分钟/小时线保留完整日期时间字符串 YYYY-MM-DD HH:MM
|
||||||
open: d.open,
|
open: d.open,
|
||||||
close: d.close,
|
close: d.close,
|
||||||
high: d.high,
|
high: d.high,
|
||||||
|
|||||||
Reference in New Issue
Block a user