feat(selection): 收紧执行状态交互与页面布局

This commit is contained in:
yuxuanhui
2026-08-10 13:42:53 +08:00
parent abe4ffc408
commit 17237e01a7
15 changed files with 659 additions and 172 deletions
@@ -0,0 +1,75 @@
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 {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog"
import { Button } from "@/shared/ui/button"
interface ExecutionStatusDrawerProps {
finalFocus: RefObject<HTMLElement | null>
onOpenChange: (open: boolean) => void
open: boolean
result: SelectionResults
}
export function ExecutionStatusDrawer({
finalFocus,
onOpenChange,
open,
result,
}: ExecutionStatusDrawerProps) {
const statusPresentation = getSelectionRunStatusPresentation(result.status)
const statusLabel = statusPresentation?.label ?? result.status
const tradeDate = result.target_trade_date ?? "未指定"
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
aria-label="选股执行状态"
className="top-0 right-0 left-auto flex h-[100svh] min-h-[100svh] w-[min(600px,100vw)] max-w-none translate-x-full translate-y-0 flex-col grid-cols-1 gap-0 rounded-l-xl rounded-r-none border-y-0 border-r-0 p-4 data-open:translate-x-0 data-closed:translate-x-full sm:max-w-none"
finalFocus={finalFocus}
id="selection-execution-status-drawer"
showCloseButton={false}
>
<DialogHeader className="shrink-0 gap-1 pr-10">
<DialogTitle className="text-lg">知行 B1 执行状态</DialogTitle>
<DialogDescription>
目标交易日:{tradeDate};当前状态:{statusLabel};未完成评估:
{result.failures.length} 条。
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto py-4">
<IncompleteEvaluationTable failures={result.failures} />
</div>
<DialogFooter className="shrink-0 -mx-4 -mb-4 rounded-none px-4 py-3">
<DialogClose render={<Button variant="outline" />}>关闭</DialogClose>
</DialogFooter>
<DialogClose
render={
<Button
aria-label="关闭执行状态"
className="absolute top-3 right-3 size-11 md:size-8"
size="icon"
variant="ghost"
/>
}
>
<XIcon aria-hidden="true" />
</DialogClose>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,85 @@
import type { SelectionFailure } from "../api/selection.types"
interface IncompleteEvaluationTableProps {
failures: SelectionFailure[]
}
const failureStatusLabels: Record<string, string> = {
data_error: "数据读取失败",
insufficient_history: "历史数据不足",
missing_target_bar: "缺少目标交易日行情",
}
export function IncompleteEvaluationTable({
failures,
}: IncompleteEvaluationTableProps) {
return (
<div className="min-w-0 overflow-hidden rounded-lg border border-border/70">
<table className="w-full table-fixed border-collapse text-left text-xs">
<caption className="sr-only">未完成评估明细</caption>
<colgroup>
<col className="w-[22%]" />
<col className="w-[24%]" />
<col className="w-[22%]" />
<col className="w-[32%]" />
</colgroup>
<thead className="bg-muted/50 text-[11px] text-muted-foreground">
<tr className="border-b border-border/70">
<th className="px-2.5 py-2 font-medium" scope="col">
股票名称
</th>
<th className="px-2.5 py-2 font-medium" scope="col">
股票代码
</th>
<th className="px-2.5 py-2 font-medium" scope="col">
评估状态
</th>
<th className="px-2.5 py-2 font-medium" scope="col">
失败原因
</th>
</tr>
</thead>
<tbody>
{failures.length > 0 ? (
failures.map((failure, index) => (
<tr
key={`${failure.ts_code}-${failure.status}-${index}`}
className="border-b border-border/60 last:border-b-0"
>
<td className="break-words px-2.5 py-2.5 align-top font-medium text-foreground">
{failure.name}
</td>
<td className="break-all px-2.5 py-2.5 align-top font-mono text-[11px] text-muted-foreground">
{failure.ts_code}
</td>
<td className="break-words px-2.5 py-2.5 align-top text-muted-foreground">
{formatFailureStatus(failure.status)}
</td>
<td className="break-words px-2.5 py-2.5 align-top leading-5 text-muted-foreground">
{formatFailureReason(failure.reason)}
</td>
</tr>
))
) : (
<tr>
<td
className="px-3 py-10 text-center text-sm text-muted-foreground"
colSpan={4}
>
暂无未完成评估
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
function formatFailureStatus(status: string) {
return failureStatusLabels[status] ?? status
}
function formatFailureReason(reason: string | null) {
return reason?.trim() || "—"
}
@@ -1,8 +1,23 @@
import type {
SelectionCategoryFilter,
SelectionRunStatus,
SelectionSignal,
} from "../api/selection.types"
export type SelectionRunStatusPresentation = {
label: string
variant: "default" | "destructive" | "outline"
}
export const selectionRunStatusPresentation: Record<
Exclude<SelectionRunStatus, "no_data" | "running">,
SelectionRunStatusPresentation
> = {
failed: { label: "执行失败", variant: "destructive" },
partial_success: { label: "部分成功", variant: "outline" },
success: { label: "执行完成", variant: "default" },
}
export const categoryLabels: Record<string, string> = {
zhixing_b1_extreme_volume: "超卖超缩量 B",
zhixing_b1_original_b1: "原始 B1",
@@ -29,6 +44,17 @@ export function getCategoryLabel(category: string) {
return categoryLabels[category] ?? category
}
export function getSelectionRunStatusPresentation(status: SelectionRunStatus) {
if (
status === "failed" ||
status === "partial_success" ||
status === "success"
) {
return selectionRunStatusPresentation[status]
}
return null
}
export function getSignalKey(signal: SelectionSignal) {
return `${signal.ts_code}-${signal.category}`
}
@@ -90,9 +90,9 @@ export function SelectionResultsWorkbench({
}
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-3 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-[420px] min-w-0 flex-1 flex-col overflow-visible rounded-lg border border-border/80 bg-card md:min-h-0 md:overflow-hidden">
<div className="grid shrink-0 gap-2 border-b border-border/60 p-3 sm:grid-cols-[minmax(0,260px)_auto_1fr] sm:items-center">
<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">
<Input
aria-label="搜索命中信号"
className="h-11 bg-background text-sm md:h-8"
@@ -120,7 +120,7 @@ export function SelectionResultsWorkbench({
</span>
</div>
<div className="flex min-w-0 flex-col md:min-h-0 md:flex-1 md:overflow-hidden">
<div className="flex min-h-0 min-w-0 flex-col md:flex-1 md:overflow-hidden">
<SignalTable
onSelect={(signal) => setSelectedKey(getSignalKey(signal))}
selectedKey={selectedSignal ? getSignalKey(selectedSignal) : null}
@@ -19,7 +19,7 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
return (
<Card
aria-label="当前股票详情"
className="hidden min-h-0 overflow-auto rounded-lg border-border/80 bg-card p-4 md:flex md:flex-col md:justify-center"
className="hidden min-h-0 overflow-auto rounded-lg border-border/80 bg-card p-3 md:flex md:flex-col md:justify-center"
>
<div className="space-y-2 text-center">
<Search
@@ -38,7 +38,7 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
return (
<aside
aria-label="当前股票详情"
className="hidden min-h-0 overflow-auto rounded-lg border border-border/80 bg-card p-4 md:block"
className="hidden min-h-0 overflow-auto rounded-lg border border-border/80 bg-card p-3 md:block"
>
<p className="text-[10px] font-bold tracking-[0.08em] text-muted-foreground uppercase">
Selected signal
@@ -58,7 +58,7 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
/>
</div>
<p className="mt-5 text-3xl font-semibold tabular-nums tracking-tight">
<p className="mt-4 text-3xl font-semibold tabular-nums tracking-tight">
¥ {signal.close.toFixed(2)}
</p>
@@ -69,7 +69,7 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
{getCategoryLabel(signal.category)}
</Badge>
<dl className="mt-5 divide-y divide-border/60 border-t border-border/60 text-xs">
<dl className="mt-4 divide-y divide-border/60 border-t border-border/60 text-xs">
<div className="flex items-center justify-between gap-3 py-2.5">
<dt className="text-muted-foreground">目标交易日</dt>
<dd className="font-medium tabular-nums">