152 lines
5.4 KiB
TypeScript
152 lines
5.4 KiB
TypeScript
import { useEffect, useMemo, useState } from "react"
|
|
import { useNavigate, useSearch } from "@tanstack/react-router"
|
|
|
|
import { Input } from "@/shared/ui/input"
|
|
import { Pagination } from "@/shared/ui/pagination"
|
|
|
|
import type { SelectionResults, SelectionSignal } from "../api/selection.types"
|
|
import { SignalDetailPanel } from "./signal-detail-panel"
|
|
import { SignalRecordList } from "./signal-record-list"
|
|
import {
|
|
getSignalKey,
|
|
matchesCategory,
|
|
signalCategoryOptions,
|
|
type SignalCategoryFilter,
|
|
} from "./selection-presentation"
|
|
import { SignalTable } from "./signal-table"
|
|
|
|
const PAGE_SIZE_OPTIONS = [5, 10, 20] as const
|
|
|
|
interface SelectionResultsWorkbenchProps {
|
|
result: SelectionResults
|
|
}
|
|
|
|
export function SelectionResultsWorkbench({
|
|
result,
|
|
}: SelectionResultsWorkbenchProps) {
|
|
const search = useSearch({ from: "/_workspace/selection" })
|
|
const navigate = useNavigate({ from: "/selection" })
|
|
const [query, setQuery] = useState("")
|
|
const [category, setCategory] = useState<SignalCategoryFilter>("all")
|
|
const [selectedKey, setSelectedKey] = useState<string | null>(null)
|
|
const [expandedKeys, setExpandedKeys] = useState<ReadonlySet<string>>(
|
|
new Set(),
|
|
)
|
|
|
|
const filteredSignals = useMemo(() => {
|
|
const normalized = query.trim().toLowerCase()
|
|
return result.signals.filter((signal) => {
|
|
const matchesQuery =
|
|
normalized.length === 0 ||
|
|
signal.name.toLowerCase().includes(normalized) ||
|
|
signal.ts_code.toLowerCase().includes(normalized)
|
|
return matchesQuery && matchesCategory(signal, category)
|
|
})
|
|
}, [category, query, result.signals])
|
|
|
|
const page = search.page
|
|
const pageSize = search.pageSize
|
|
const pageCount = Math.max(1, Math.ceil(filteredSignals.length / pageSize))
|
|
const currentPage = Math.min(page, pageCount)
|
|
const currentSignals = filteredSignals.slice(
|
|
(currentPage - 1) * pageSize,
|
|
currentPage * pageSize,
|
|
)
|
|
const selectedSignal =
|
|
currentSignals.find((signal) => getSignalKey(signal) === selectedKey) ??
|
|
currentSignals[0]
|
|
|
|
useEffect(() => {
|
|
if (page > pageCount) {
|
|
void navigate({
|
|
search: (previous) => ({ ...previous, page: pageCount }),
|
|
})
|
|
}
|
|
}, [navigate, page, pageCount])
|
|
|
|
function updateSearch(next: { page?: number; pageSize?: number }) {
|
|
void navigate({ search: (previous) => ({ ...previous, ...next }) })
|
|
}
|
|
|
|
function handleQueryChange(value: string) {
|
|
setQuery(value)
|
|
updateSearch({ page: 1 })
|
|
}
|
|
|
|
function handleCategoryChange(value: SignalCategoryFilter) {
|
|
setCategory(value)
|
|
updateSearch({ page: 1 })
|
|
}
|
|
|
|
function handleToggleExpanded(signal: SelectionSignal) {
|
|
const key = getSignalKey(signal)
|
|
setExpandedKeys((previous) => {
|
|
const next = new Set(previous)
|
|
if (next.has(key)) next.delete(key)
|
|
else next.add(key)
|
|
return next
|
|
})
|
|
}
|
|
|
|
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">
|
|
<Input
|
|
aria-label="搜索命中信号"
|
|
className="h-11 bg-background text-sm 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)
|
|
}
|
|
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">
|
|
筛选结果 {filteredSignals.length} 条
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 flex-col md:min-h-0 md:flex-1 md:overflow-hidden">
|
|
<SignalTable
|
|
onSelect={(signal) => setSelectedKey(getSignalKey(signal))}
|
|
selectedKey={selectedSignal ? getSignalKey(selectedSignal) : null}
|
|
signals={currentSignals}
|
|
/>
|
|
<SignalRecordList
|
|
expandedKeys={expandedKeys}
|
|
onSelect={(signal) => setSelectedKey(getSignalKey(signal))}
|
|
onToggleExpanded={handleToggleExpanded}
|
|
signals={currentSignals}
|
|
/>
|
|
</div>
|
|
|
|
<Pagination
|
|
onPageChange={(nextPage) => updateSearch({ page: nextPage })}
|
|
onPageSizeChange={(nextPageSize) =>
|
|
updateSearch({ page: 1, pageSize: nextPageSize })
|
|
}
|
|
page={currentPage}
|
|
pageSize={pageSize}
|
|
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
total={filteredSignals.length}
|
|
/>
|
|
</section>
|
|
|
|
<SignalDetailPanel signal={selectedSignal} />
|
|
</div>
|
|
)
|
|
}
|