Develop #28
@@ -16,7 +16,7 @@ export function PatternCaseImage({ score }: PatternCaseImageProps) {
|
||||
patternCaseImageById[matchedCase.id as keyof typeof patternCaseImageById]
|
||||
|
||||
return (
|
||||
<section aria-labelledby="best-pattern-case" className="mt-4">
|
||||
<section aria-labelledby="best-pattern-case" className="mt-2">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-3">
|
||||
<h3 className="text-xs font-medium" id="best-pattern-case">
|
||||
最接近案例:{matchedCase.name}
|
||||
@@ -32,7 +32,7 @@ export function PatternCaseImage({ score }: PatternCaseImageProps) {
|
||||
) : (
|
||||
<img
|
||||
alt={`${matchedCase.name} B1 参考案例图`}
|
||||
className="aspect-2/1 w-full rounded-md border border-border/60 object-contain"
|
||||
className="h-40 w-full rounded-md border border-border/60 object-contain"
|
||||
loading="lazy"
|
||||
onError={() => setFailedSource(source)}
|
||||
src={source}
|
||||
|
||||
@@ -6,6 +6,13 @@ interface PatternScoreProps {
|
||||
score: SelectionPatternScore | null
|
||||
}
|
||||
|
||||
const breakdownLabels = {
|
||||
trend_structure: "趋势",
|
||||
kdj_state: "KDJ",
|
||||
volume_pattern: "量能",
|
||||
price_shape: "价格形态",
|
||||
} as const
|
||||
|
||||
export function PatternScoreSummary({ score }: PatternScoreProps) {
|
||||
if (!score) return <span className="text-muted-foreground">未评分</span>
|
||||
if (score.status === "failed") {
|
||||
@@ -44,9 +51,20 @@ export function PatternScoreDetails({ score }: PatternScoreProps) {
|
||||
<p className="text-xl font-semibold tabular-nums">
|
||||
{formatScore(score.value)} 分
|
||||
</p>
|
||||
<Badge variant={score.status === "matched" ? "default" : "outline"}>
|
||||
{score.status === "matched" ? "已匹配" : "低于匹配阈值"}
|
||||
</Badge>
|
||||
{score.breakdown ? (
|
||||
<dl className="grid grid-cols-2 gap-1.5 text-xs">
|
||||
{Object.entries(breakdownLabels).map(([key, label]) => (
|
||||
<div key={key} className="rounded-md border border-border/60 p-1.5">
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="mt-0.5 font-semibold tabular-nums">
|
||||
{formatScore(
|
||||
score.breakdown?.[key as keyof typeof breakdownLabels],
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ describe("selection chart option", () => {
|
||||
})
|
||||
expect(dataZoom[1].xAxisIndex).toEqual([0, 1, 2])
|
||||
expect(legends[0]?.data).toEqual(["知行白线", "知行黄线"])
|
||||
expect(legends[1]?.data).toEqual(["J"])
|
||||
expect(series.find((item) => item.name === "日线")).toMatchObject({
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
@@ -62,7 +63,16 @@ describe("selection chart option", () => {
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
})
|
||||
expect(series.filter((item) => item.xAxisIndex === 2)).toHaveLength(3)
|
||||
expect(series.filter((item) => item.xAxisIndex === 2)).toEqual([
|
||||
expect.objectContaining({
|
||||
data: points.map((item) => item.j),
|
||||
name: "J",
|
||||
xAxisIndex: 2,
|
||||
yAxisIndex: 2,
|
||||
}),
|
||||
])
|
||||
expect(series.find((item) => item.name === "K")).toBeUndefined()
|
||||
expect(series.find((item) => item.name === "D")).toBeUndefined()
|
||||
expect(series[0].data[0]).toEqual([10, 10.5, 9, 11])
|
||||
})
|
||||
|
||||
@@ -91,7 +101,7 @@ describe("selection chart option", () => {
|
||||
expect(candle).toHaveLength(4)
|
||||
expect(candle.every(Number.isNaN)).toBe(true)
|
||||
expect(volume?.data[0]).toMatchObject({ value: null })
|
||||
expect(lines).toHaveLength(5)
|
||||
expect(lines).toHaveLength(3)
|
||||
for (const indicator of lines) {
|
||||
expect(indicator.data).toEqual([null])
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export type SelectionChartOption = ComposeOption<
|
||||
/**
|
||||
* Build the three-grid financial chart configuration from aligned API points.
|
||||
*
|
||||
* @param points Ascending qfq OHLCV, KDJ, and Zhixing-line points from the API.
|
||||
* @param points Ascending qfq OHLCV, J, and Zhixing-line points from the API.
|
||||
* @returns One ECharts option with synchronized axes and an exact 120-point window.
|
||||
*/
|
||||
export function buildSelectionChartOption(
|
||||
@@ -75,7 +75,7 @@ export function buildSelectionChartOption(
|
||||
top: 8,
|
||||
},
|
||||
{
|
||||
data: ["K", "D", "J"],
|
||||
data: ["J"],
|
||||
right: 20,
|
||||
top: "73%",
|
||||
},
|
||||
@@ -126,25 +126,25 @@ export function buildSelectionChartOption(
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
...(["k", "d", "j"] as const).map((key, index): LineSeriesOption => ({
|
||||
{
|
||||
connectNulls: false,
|
||||
data: points.map((point) => point[key]),
|
||||
name: key.toUpperCase(),
|
||||
data: points.map((point) => point.j),
|
||||
lineStyle: {
|
||||
color: "#9333ea",
|
||||
width: 1.5,
|
||||
},
|
||||
name: "J",
|
||||
showSymbol: false,
|
||||
smooth: false,
|
||||
type: "line",
|
||||
xAxisIndex: 2,
|
||||
yAxisIndex: 2,
|
||||
lineStyle: {
|
||||
color: ["#2563eb", "#d97706", "#9333ea"][index],
|
||||
width: 1.5,
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
title: [
|
||||
{ left: 12, text: "日线", textStyle: { fontSize: 12 }, top: 8 },
|
||||
{ left: 72, text: "成交量", textStyle: { fontSize: 12 }, top: "54%" },
|
||||
{ left: 72, text: "KDJ", textStyle: { fontSize: 12 }, top: "72%" },
|
||||
{ left: 72, text: "J", textStyle: { fontSize: 12 }, top: "72%" },
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: { type: "cross" },
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("SelectionChart", () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole("img", {
|
||||
name: "药明康德 日线、知行黄白线、成交量和 KDJ 图表",
|
||||
name: "药明康德 日线、知行黄白线、成交量和 J 线图表",
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(chartMocks.init).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -64,7 +64,7 @@ export function SelectionChart({ chart, className }: SelectionChartProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={`${chart.name} 日线、知行黄白线、成交量和 KDJ 图表`}
|
||||
aria-label={`${chart.name} 日线、知行黄白线、成交量和 J 线图表`}
|
||||
className={cn("h-full min-h-70 w-full", className)}
|
||||
ref={containerRef}
|
||||
role="img"
|
||||
|
||||
@@ -212,7 +212,10 @@ describe("SelectionResultsPage", () => {
|
||||
expect(
|
||||
within(detailPanel).getByText("最接近案例:华纳药厂"),
|
||||
).toBeInTheDocument()
|
||||
expect(within(detailPanel).queryByText("价格形态")).not.toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("趋势")).toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("量能")).toBeInTheDocument()
|
||||
expect(within(detailPanel).queryByText("已匹配")).not.toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
|
||||
expect(within(detailPanel).getByText("日涨跌幅")).toBeInTheDocument()
|
||||
@@ -239,7 +242,7 @@ describe("SelectionResultsPage", () => {
|
||||
score: null,
|
||||
},
|
||||
{
|
||||
expected: "低于匹配阈值",
|
||||
expected: "低于阈值",
|
||||
score: {
|
||||
breakdown: {
|
||||
kdj_state: 42,
|
||||
|
||||
Reference in New Issue
Block a user