feat(selection): 重构选股结果筛选与状态入口
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user