feat(selection): 重构选股结果筛选与状态入口

This commit is contained in:
yuxuanhui
2026-08-10 14:50:56 +08:00
parent 78be0f53d4
commit 8d4f80f2f1
17 changed files with 1123 additions and 156 deletions
@@ -1,9 +1,8 @@
import { XIcon } from "lucide-react"
import type { RefObject } from "react"
import type { SelectionResults } from "../api/selection.types"
import { IncompleteEvaluationTable } from "./incomplete-evaluation-table"
import { getSelectionRunStatusPresentation } from "./selection-presentation"
import { Badge } from "@/shared/ui/badge"
import { Button } from "@/shared/ui/button"
import {
Dialog,
DialogClose,
@@ -13,7 +12,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog"
import { Button } from "@/shared/ui/button"
import type { SelectionResults } from "../api/selection.types"
import { IncompleteEvaluationTable } from "./incomplete-evaluation-table"
import { getSelectionRunStatusPresentation } from "./selection-presentation"
interface ExecutionStatusDrawerProps {
finalFocus: RefObject<HTMLElement | null>
@@ -50,6 +52,37 @@ export function ExecutionStatusDrawer({
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto py-4">
<section
aria-label="执行摘要"
className="mb-4 rounded-lg border border-border/70 bg-muted/20 p-3"
>
<h2 className="sr-only">执行摘要</h2>
<dl className="grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-3">
<SummaryMetric label="目标股票数" value={result.target_count} />
<SummaryMetric label="实际参与数" value={result.eligible_count} />
<SummaryMetric
label="命中股票数"
value={result.selected_stock_count}
/>
<SummaryMetric label="信号条数" value={result.signal_count} />
<SummaryMetric
label="数据覆盖率"
value={`${(result.coverage * 100).toFixed(2)}%`}
/>
<div className="space-y-1">
<dt className="text-[11px] text-muted-foreground">执行状态</dt>
<dd>
{statusPresentation ? (
<Badge variant={statusPresentation.variant}>
{statusLabel}
</Badge>
) : (
<span className="text-sm font-semibold">{statusLabel}</span>
)}
</dd>
</div>
</dl>
</section>
<IncompleteEvaluationTable failures={result.failures} />
</div>
@@ -73,3 +106,20 @@ export function ExecutionStatusDrawer({
</Dialog>
)
}
function SummaryMetric({
label,
value,
}: {
label: string
value: number | string
}) {
return (
<div className="space-y-1">
<dt className="text-[11px] text-muted-foreground">{label}</dt>
<dd className="text-base font-semibold tracking-tight tabular-nums">
{value}
</dd>
</div>
)
}
@@ -0,0 +1,49 @@
import { CheckCircle2 } from "lucide-react"
import type { RefObject } from "react"
import { Badge } from "@/shared/ui/badge"
import { Button } from "@/shared/ui/button"
import type { SelectionResults } from "../api/selection.types"
import { getSelectionRunStatusPresentation } from "./selection-presentation"
interface ExecutionStatusTriggerProps {
drawerOpen: boolean
onOpen: () => void
result: SelectionResults
triggerRef: RefObject<HTMLButtonElement | null>
}
export function ExecutionStatusTrigger({
drawerOpen,
onOpen,
result,
triggerRef,
}: ExecutionStatusTriggerProps) {
const statusPresentation = getSelectionRunStatusPresentation(result.status)
if (!statusPresentation) return null
return (
<Button
aria-controls="selection-execution-status-drawer"
aria-expanded={drawerOpen}
aria-haspopup="dialog"
aria-label={`查看执行状态:${statusPresentation.label},未完成评估 ${result.failures.length} 条`}
className="h-auto min-h-11 shrink-0 justify-start gap-1.5 px-2 md:min-h-8"
onClick={onOpen}
ref={triggerRef}
variant="outline"
>
<Badge className="gap-1" variant={statusPresentation.variant}>
{result.status === "success" ? (
<CheckCircle2 aria-hidden="true" />
) : null}
{statusPresentation.label}
</Badge>
<span className="text-[11px] text-muted-foreground">
{result.failures.length} 条未完成
</span>
</Button>
)
}
@@ -1,10 +1,19 @@
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo, useState, type RefObject } from "react"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { Input } from "@/shared/ui/input"
import { Pagination } from "@/shared/ui/pagination"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select"
import type { SelectionResults, SelectionSignal } from "../api/selection.types"
import { ExecutionStatusTrigger } from "./execution-status-trigger"
import { SignalDetailPanel } from "./signal-detail-panel"
import { SignalRecordList } from "./signal-record-list"
import {
@@ -18,11 +27,17 @@ import { SignalTable } from "./signal-table"
const PAGE_SIZE_OPTIONS = [5, 10, 15] as const
interface SelectionResultsWorkbenchProps {
drawerOpen: boolean
onOpenExecutionStatus: () => void
result: SelectionResults
triggerRef: RefObject<HTMLButtonElement | null>
}
export function SelectionResultsWorkbench({
drawerOpen,
onOpenExecutionStatus,
result,
triggerRef,
}: SelectionResultsWorkbenchProps) {
const search = useSearch({ from: "/_workspace/selection" })
const navigate = useNavigate({ from: "/selection" })
@@ -53,7 +68,6 @@ export function SelectionResultsWorkbench({
const selectedSignal =
currentSignals.find((signal) => getSignalKey(signal) === selectedKey) ??
currentSignals[0]
useEffect(() => {
if (page > pageCount) {
void navigate({
@@ -92,32 +106,49 @@ export function SelectionResultsWorkbench({
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-2 overflow-visible md:grid md:grid-cols-[minmax(0,1fr)_260px] md:overflow-hidden lg:grid-cols-[minmax(0,1.55fr)_minmax(280px,0.72fr)]">
<section className="flex min-h-0 min-w-0 flex-1 flex-col overflow-visible rounded-lg border border-border/80 bg-card md:overflow-hidden">
<div className="grid shrink-0 gap-1.5 border-b border-border/60 p-2.5 sm:grid-cols-[minmax(0,260px)_auto_1fr] sm:items-center">
<div className="flex shrink-0 flex-wrap items-center gap-1.5 border-b border-border/60 p-2.5">
<Input
aria-label="搜索命中信号"
className="h-11 bg-background text-sm md:h-8"
className="h-11 min-w-0 flex-1 bg-background text-sm sm:max-w-[260px] md:h-8"
onChange={(event) => handleQueryChange(event.target.value)}
placeholder="搜索股票名称或代码"
type="search"
value={query}
/>
<select
aria-label="筛选信号分类"
className="h-11 rounded-lg border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:h-8"
onChange={(event) =>
handleCategoryChange(event.target.value as SignalCategoryFilter)
}
<Select
items={signalCategoryOptions}
onValueChange={(value) => {
if (typeof value === "string") {
handleCategoryChange(value as SignalCategoryFilter)
}
}}
value={category}
>
{signalCategoryOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<span className="text-xs tabular-nums text-muted-foreground sm:justify-self-end">
<SelectTrigger
aria-label="筛选信号分类"
className="h-11 w-full bg-background text-sm sm:w-auto sm:min-w-36 md:h-8"
>
<SelectValue placeholder="筛选信号分类" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{signalCategoryOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<span className="text-xs tabular-nums text-muted-foreground sm:ml-auto">
筛选结果 {signalsTotal} 条
</span>
<ExecutionStatusTrigger
drawerOpen={drawerOpen}
onOpen={onOpenExecutionStatus}
result={result}
triggerRef={triggerRef}
/>
</div>
<div className="flex min-h-0 min-w-0 flex-col md:flex-1 md:overflow-hidden">