Develop #28

Merged
sakibcc merged 2 commits from develop into main 2026-09-02 18:39:32 +08:00
7 changed files with 53 additions and 22 deletions
@@ -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,