Merge pull request 'Develop' (#28) from develop into main
Deploy Production / deploy (push) Successful in 27s

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