feat(selection): 收紧执行状态交互与页面布局
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user