feat(sector-radar): add IME-safe sector autocomplete and detail lookup
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
useSectorRadarHistory,
|
useSectorRadarHistory,
|
||||||
sectorRadarDatesQueryKey,
|
sectorRadarDatesQueryKey,
|
||||||
sectorRadarRankingsQueryKey,
|
sectorRadarRankingsQueryKey,
|
||||||
|
useSectorRadarSuggestions,
|
||||||
useSectorRadarDates,
|
useSectorRadarDates,
|
||||||
useSectorRadarRankings,
|
useSectorRadarRankings,
|
||||||
} from "./sector-radar.query"
|
} from "./sector-radar.query"
|
||||||
@@ -41,6 +42,37 @@ describe("sector radar query hooks", () => {
|
|||||||
useInfiniteQuery.mockImplementation((options) => options)
|
useInfiniteQuery.mockImplementation((options) => options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("searches both complete pools at the requested date with cancellation", async () => {
|
||||||
|
api.getSectorRadarRankings.mockResolvedValue({ rows: [], total: 0 })
|
||||||
|
useSectorRadarSuggestions(" 医疗 ", "2026-08-28")
|
||||||
|
const options = useQuery.mock.calls.at(-1)?.[0]
|
||||||
|
const signal = new AbortController().signal
|
||||||
|
expect(options.enabled).toBe(true)
|
||||||
|
expect(options.queryKey).toEqual([
|
||||||
|
"sectorRadar",
|
||||||
|
"suggestions",
|
||||||
|
"2026-08-28",
|
||||||
|
"医疗",
|
||||||
|
])
|
||||||
|
await options.queryFn({ signal })
|
||||||
|
for (const sectorType of ["concept", "industry"]) {
|
||||||
|
expect(api.getSectorRadarRankings).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
sectorType,
|
||||||
|
tradeDate: "2026-08-28",
|
||||||
|
side: "all",
|
||||||
|
search: "医疗",
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
useSectorRadarSuggestions(" ")
|
||||||
|
expect(useQuery.mock.calls.at(-1)?.[0].enabled).toBe(false)
|
||||||
|
useSectorRadarSuggestions("医疗", undefined, false)
|
||||||
|
expect(useQuery.mock.calls.at(-1)?.[0].enabled).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it("isolates sector resources by type, code and date and forwards cancellation", () => {
|
it("isolates sector resources by type, code and date and forwards cancellation", () => {
|
||||||
const sector = {
|
const sector = {
|
||||||
sectorType: "concept" as const,
|
sectorType: "concept" as const,
|
||||||
|
|||||||
@@ -127,3 +127,45 @@ export function useSectorRadarDetail(query: RadarSectorQuery) {
|
|||||||
queryFn: ({ signal }) => getSectorRadarDetail(query, signal),
|
queryFn: ({ signal }) => getSectorRadarDetail(query, signal),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Search both published sector pools, independently of the visible top/bottom lists. */
|
||||||
|
export function useSectorRadarSuggestions(
|
||||||
|
search: string,
|
||||||
|
tradeDate?: string,
|
||||||
|
enabled = true,
|
||||||
|
) {
|
||||||
|
const term = search.trim()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"sectorRadar",
|
||||||
|
"suggestions",
|
||||||
|
tradeDate ?? "latest",
|
||||||
|
term,
|
||||||
|
] as const,
|
||||||
|
enabled: enabled && Boolean(term),
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const pages = await Promise.all(
|
||||||
|
(["concept", "industry"] as const).map((sectorType) =>
|
||||||
|
getSectorRadarRankings(
|
||||||
|
{
|
||||||
|
tradeDate,
|
||||||
|
sectorType,
|
||||||
|
view: "amount",
|
||||||
|
rankChangeMetric: "amount",
|
||||||
|
rankChangeDays: 1,
|
||||||
|
side: "all",
|
||||||
|
search: term,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
rows: pages.flatMap((page) => page.rows),
|
||||||
|
total: pages.reduce((sum, page) => sum + page.total, 0),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function RadarDetailDialog({ row }: { row: RadarRankingRow }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadarDetail({ row }: { row: RadarRankingRow }) {
|
export function RadarDetail({ row }: { row: RadarRankingRow }) {
|
||||||
const query: RadarSectorQuery = {
|
const query: RadarSectorQuery = {
|
||||||
sectorType: row.sector_type,
|
sectorType: row.sector_type,
|
||||||
sectorCode: row.sector_code,
|
sectorCode: row.sector_code,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Autocomplete } from "@base-ui/react/autocomplete"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
import { Button } from "@/shared/ui/button"
|
||||||
|
import { Dialog, DialogContent } from "@/shared/ui/dialog"
|
||||||
|
import { Input } from "@/shared/ui/input"
|
||||||
|
import { useSectorRadarSuggestions } from "../api/sector-radar.query"
|
||||||
|
import type { RadarRankingRow } from "../api/sector-radar.types"
|
||||||
|
import { RadarDetail } from "./radar-detail-dialog"
|
||||||
|
|
||||||
|
/** Keep IME drafts local; only completed input may trigger a debounced lookup. */
|
||||||
|
export function RadarSectorSearch({ tradeDate }: { tradeDate?: string }) {
|
||||||
|
const [value, setValue] = useState("")
|
||||||
|
const [term, setTerm] = useState("")
|
||||||
|
const [composing, setComposing] = useState(false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [selected, setSelected] = useState<RadarRankingRow | null>(null)
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (composing) return
|
||||||
|
const timer = setTimeout(() => setTerm(value.trim()), 250)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [value, composing])
|
||||||
|
const ready = !composing && Boolean(term) && term === value.trim()
|
||||||
|
const suggestions = useSectorRadarSuggestions(term, tradeDate, ready && open)
|
||||||
|
const rows = ready ? (suggestions.data?.rows ?? []) : []
|
||||||
|
const popupOpen = open && !composing && Boolean(value.trim()) && !detailOpen
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-1 text-xs font-medium text-muted-foreground lg:col-span-2">
|
||||||
|
<span>搜索板块</span>
|
||||||
|
<Autocomplete.Root
|
||||||
|
items={rows}
|
||||||
|
filter={null}
|
||||||
|
value={value}
|
||||||
|
onValueChange={setValue}
|
||||||
|
open={popupOpen}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
openOnInputClick
|
||||||
|
itemToStringValue={(row: RadarRankingRow) => row.sector_name}
|
||||||
|
>
|
||||||
|
<Autocomplete.Input
|
||||||
|
ref={inputRef}
|
||||||
|
aria-label="搜索板块"
|
||||||
|
placeholder="名称或代码"
|
||||||
|
maxLength={100}
|
||||||
|
render={<Input className="bg-background" />}
|
||||||
|
onCompositionStart={() => setComposing(true)}
|
||||||
|
onCompositionEnd={(event) => {
|
||||||
|
setValue(event.currentTarget.value)
|
||||||
|
setComposing(false)
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Autocomplete.Portal>
|
||||||
|
<Autocomplete.Positioner sideOffset={4} align="end" className="z-50">
|
||||||
|
<Autocomplete.Popup className="w-[max(var(--anchor-width),20rem)] max-w-[calc(100vw-2rem)] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md">
|
||||||
|
{!ready || suggestions.isPending ? (
|
||||||
|
<p
|
||||||
|
role="status"
|
||||||
|
className="px-3 py-3 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
正在搜索板块…
|
||||||
|
</p>
|
||||||
|
) : suggestions.isError ? (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="flex items-center justify-between gap-2 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
搜索失败,请重试。
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void suggestions.refetch()}
|
||||||
|
>
|
||||||
|
重试搜索
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<p
|
||||||
|
role="status"
|
||||||
|
className="px-3 py-3 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
未找到匹配的板块。
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Autocomplete.List className="max-h-72 overflow-y-auto">
|
||||||
|
{(row: RadarRankingRow) => (
|
||||||
|
<Autocomplete.Item
|
||||||
|
key={`${row.sector_type}:${row.sector_code}`}
|
||||||
|
value={row}
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(row)
|
||||||
|
setDetailOpen(true)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer items-center gap-3 border-b border-border/60 px-3 py-3 text-sm outline-none last:border-0 data-highlighted:bg-accent data-highlighted:text-accent-foreground"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="font-medium">{row.sector_name}</span>{" "}
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{row.sector_code}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{row.sector_type === "concept" ? "概念" : "行业"}
|
||||||
|
</span>
|
||||||
|
</Autocomplete.Item>
|
||||||
|
)}
|
||||||
|
</Autocomplete.List>
|
||||||
|
{ready &&
|
||||||
|
suggestions.data &&
|
||||||
|
suggestions.data.total > rows.length ? (
|
||||||
|
<p className="border-t border-border px-3 py-2 text-xs text-muted-foreground">
|
||||||
|
仅展示部分结果,请补充名称或代码缩小范围。
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</Autocomplete.Popup>
|
||||||
|
</Autocomplete.Positioner>
|
||||||
|
</Autocomplete.Portal>
|
||||||
|
</Autocomplete.Root>
|
||||||
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||||
|
<DialogContent
|
||||||
|
finalFocus={inputRef}
|
||||||
|
className="max-h-[90dvh] grid-cols-1 overflow-y-auto sm:max-w-4xl"
|
||||||
|
>
|
||||||
|
{detailOpen && selected ? <RadarDetail row={selected} /> : null}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { fireEvent, render, screen, within } from "@testing-library/react"
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
within,
|
||||||
|
} from "@testing-library/react"
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -14,6 +21,7 @@ const useSectorRadarDates = vi.fn()
|
|||||||
const useSectorRadarRankings = vi.fn()
|
const useSectorRadarRankings = vi.fn()
|
||||||
const useSectorRadarHistory = vi.fn()
|
const useSectorRadarHistory = vi.fn()
|
||||||
const useSectorRadarDetail = vi.fn()
|
const useSectorRadarDetail = vi.fn()
|
||||||
|
const useSectorRadarSuggestions = vi.fn()
|
||||||
const refetchDates = vi.fn()
|
const refetchDates = vi.fn()
|
||||||
const refetchRankings = vi.fn()
|
const refetchRankings = vi.fn()
|
||||||
const fetchNextPage = vi.fn()
|
const fetchNextPage = vi.fn()
|
||||||
@@ -26,6 +34,8 @@ vi.mock("@tanstack/react-router", () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("@/features/sector-radar/api/sector-radar.query", () => ({
|
vi.mock("@/features/sector-radar/api/sector-radar.query", () => ({
|
||||||
|
useSectorRadarSuggestions: (...args: unknown[]) =>
|
||||||
|
useSectorRadarSuggestions(...args),
|
||||||
useSectorRadarDetail: (...args: unknown[]) => useSectorRadarDetail(...args),
|
useSectorRadarDetail: (...args: unknown[]) => useSectorRadarDetail(...args),
|
||||||
useSectorRadarHistory: (...args: unknown[]) => useSectorRadarHistory(...args),
|
useSectorRadarHistory: (...args: unknown[]) => useSectorRadarHistory(...args),
|
||||||
useSectorRadarDates: () => useSectorRadarDates(),
|
useSectorRadarDates: () => useSectorRadarDates(),
|
||||||
@@ -183,6 +193,11 @@ describe("SectorRadarPage", () => {
|
|||||||
data: { status: "no_data" },
|
data: { status: "no_data" },
|
||||||
})
|
})
|
||||||
useSectorRadarDetail.mockReturnValue({ isPending: true })
|
useSectorRadarDetail.mockReturnValue({ isPending: true })
|
||||||
|
useSectorRadarSuggestions.mockReturnValue({
|
||||||
|
isPending: false,
|
||||||
|
isError: false,
|
||||||
|
data: { rows: [], total: 0 },
|
||||||
|
})
|
||||||
navigate.mockReset()
|
navigate.mockReset()
|
||||||
refetchDates.mockReset()
|
refetchDates.mockReset()
|
||||||
refetchRankings.mockReset()
|
refetchRankings.mockReset()
|
||||||
@@ -358,23 +373,138 @@ describe("SectorRadarPage", () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
it("resets filters to the first batch in router search state", () => {
|
it("keeps Chinese composition local and searches only after committing it", async () => {
|
||||||
routeSearch = { ...routeSearch, page: 3 }
|
|
||||||
render(<SectorRadarPage />)
|
render(<SectorRadarPage />)
|
||||||
|
const input = screen.getByRole("combobox", { name: "搜索板块" })
|
||||||
fireEvent.change(screen.getByRole("searchbox", { name: "搜索板块" }), {
|
fireEvent.focus(input)
|
||||||
target: { value: "机器人" },
|
fireEvent.compositionStart(input)
|
||||||
|
fireEvent.change(input, { target: { value: "yi liao" } })
|
||||||
|
expect(input).toHaveValue("yi liao")
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
})
|
})
|
||||||
|
expect(useSectorRadarSuggestions).toHaveBeenLastCalledWith(
|
||||||
const searchCall = navigate.mock.calls[0]?.[0].search as (
|
"",
|
||||||
previous: SectorRadarRouteSearch,
|
undefined,
|
||||||
) => SectorRadarRouteSearch
|
false,
|
||||||
expect(searchCall({ ...routeSearch, page: 3 })).toMatchObject({
|
)
|
||||||
page: 1,
|
expect(navigate).not.toHaveBeenCalled()
|
||||||
search: "机器人",
|
fireEvent.input(input, {
|
||||||
|
target: { value: "医疗" },
|
||||||
|
inputType: "insertText",
|
||||||
})
|
})
|
||||||
|
fireEvent.compositionEnd(input, { data: "医疗" })
|
||||||
|
expect(input).toHaveValue("医疗")
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useSectorRadarSuggestions).toHaveBeenLastCalledWith(
|
||||||
|
"医疗",
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(navigate).not.toHaveBeenCalled()
|
||||||
|
fireEvent.change(input, { target: { value: "" } })
|
||||||
|
expect(input).toHaveValue("")
|
||||||
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("hides stale suggestions while the next search is waiting", async () => {
|
||||||
|
useSectorRadarSuggestions.mockReturnValue({
|
||||||
|
isPending: false,
|
||||||
|
isError: false,
|
||||||
|
data: { rows: [rankingsResponse.rows[0]], total: 1 },
|
||||||
|
})
|
||||||
|
render(<SectorRadarPage />)
|
||||||
|
const input = screen.getByRole("combobox", { name: "搜索板块" })
|
||||||
|
fireEvent.input(input, {
|
||||||
|
target: { value: "机器人" },
|
||||||
|
inputType: "insertText",
|
||||||
|
})
|
||||||
|
expect(await screen.findByRole("option")).toHaveTextContent("机器人")
|
||||||
|
fireEvent.input(input, {
|
||||||
|
target: { value: "医疗" },
|
||||||
|
inputType: "insertText",
|
||||||
|
})
|
||||||
|
expect(screen.queryByRole("option")).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText("正在搜索板块…")).toBeInTheDocument()
|
||||||
|
fireEvent.keyDown(input, { key: "Escape" })
|
||||||
|
expect(input).toHaveAttribute("aria-expanded", "false")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shows empty and failed searches with retry", async () => {
|
||||||
|
const retry = vi.fn()
|
||||||
|
const { rerender } = render(<SectorRadarPage />)
|
||||||
|
const input = screen.getByRole("combobox", { name: "搜索板块" })
|
||||||
|
fireEvent.input(input, {
|
||||||
|
target: { value: "医疗" },
|
||||||
|
inputType: "insertText",
|
||||||
|
})
|
||||||
|
expect(await screen.findByText("未找到匹配的板块。")).toBeInTheDocument()
|
||||||
|
useSectorRadarSuggestions.mockReturnValue({
|
||||||
|
isPending: false,
|
||||||
|
isError: true,
|
||||||
|
refetch: retry,
|
||||||
|
})
|
||||||
|
rerender(<SectorRadarPage />)
|
||||||
|
expect(screen.getByRole("alert")).toHaveTextContent("搜索失败")
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "重试搜索" }))
|
||||||
|
expect(retry).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(["pointer", "keyboard"])(
|
||||||
|
"opens mixed sector suggestions using %s and restores focus",
|
||||||
|
async (method) => {
|
||||||
|
useSectorRadarSuggestions.mockReturnValue({
|
||||||
|
isPending: false,
|
||||||
|
isError: false,
|
||||||
|
data: {
|
||||||
|
total: 2,
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
...rankingsResponse.rows[0],
|
||||||
|
sector_name: "创新医疗服务",
|
||||||
|
sector_code: "BK1658.DC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...rankingsResponse.rows[0],
|
||||||
|
sector_name: "医疗服务",
|
||||||
|
sector_code: "BK0727.DC",
|
||||||
|
sector_type: "industry",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
render(<SectorRadarPage />)
|
||||||
|
const input = screen.getByRole("combobox", { name: "搜索板块" })
|
||||||
|
fireEvent.focus(input)
|
||||||
|
fireEvent.input(input, {
|
||||||
|
target: { value: "医疗" },
|
||||||
|
inputType: "insertText",
|
||||||
|
})
|
||||||
|
const option = await screen.findByRole("option", {
|
||||||
|
name: "医疗服务 BK0727.DC 行业",
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
screen.getByRole("option", { name: "创新医疗服务 BK1658.DC 概念" }),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
if (method === "pointer") fireEvent.click(option)
|
||||||
|
else {
|
||||||
|
fireEvent.keyDown(input, { key: "ArrowDown" })
|
||||||
|
fireEvent.keyDown(input, { key: "ArrowDown" })
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" })
|
||||||
|
}
|
||||||
|
const dialog = await screen.findByRole("dialog", { name: "医疗服务" })
|
||||||
|
expect(useSectorRadarDetail).toHaveBeenLastCalledWith({
|
||||||
|
sectorType: "industry",
|
||||||
|
sectorCode: "BK0727.DC",
|
||||||
|
tradeDate: "2026-08-28",
|
||||||
|
})
|
||||||
|
expect(navigate).not.toHaveBeenCalled()
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Close" }))
|
||||||
|
await waitFor(() => expect(input).toHaveFocus())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
it("renders accessible tabs, writes tab state to the URL, and queries both sides", () => {
|
it("renders accessible tabs, writes tab state to the URL, and queries both sides", () => {
|
||||||
render(<SectorRadarPage />)
|
render(<SectorRadarPage />)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/shared/ui/card"
|
} from "@/shared/ui/card"
|
||||||
import { Input } from "@/shared/ui/input"
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
} from "@/shared/ui/select"
|
} from "@/shared/ui/select"
|
||||||
import { Skeleton } from "@/shared/ui/skeleton"
|
import { Skeleton } from "@/shared/ui/skeleton"
|
||||||
|
|
||||||
|
import { RadarSectorSearch } from "../components/radar-sector-search"
|
||||||
import { RadarDetailDialog } from "../components/radar-detail-dialog"
|
import { RadarDetailDialog } from "../components/radar-detail-dialog"
|
||||||
import { RadarHistory } from "../components/radar-history"
|
import { RadarHistory } from "../components/radar-history"
|
||||||
import { formatRadarValue, radarValueTone } from "../components/radar-format"
|
import { formatRadarValue, radarValueTone } from "../components/radar-format"
|
||||||
@@ -68,7 +68,6 @@ export function SectorRadarPage() {
|
|||||||
view: search.view,
|
view: search.view,
|
||||||
rankChangeMetric: search.rankChangeMetric,
|
rankChangeMetric: search.rankChangeMetric,
|
||||||
rankChangeDays: search.rankChangeDays,
|
rankChangeDays: search.rankChangeDays,
|
||||||
search: search.search || undefined,
|
|
||||||
page: search.page,
|
page: search.page,
|
||||||
pageSize: search.pageSize,
|
pageSize: search.pageSize,
|
||||||
}
|
}
|
||||||
@@ -229,20 +228,10 @@ function RadarFilters({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<label className="grid gap-1 text-xs font-medium text-muted-foreground lg:col-span-2">
|
<RadarSectorSearch
|
||||||
搜索板块
|
key={search.tradeDate ?? "latest"}
|
||||||
<Input
|
tradeDate={search.tradeDate}
|
||||||
aria-label="搜索板块"
|
/>
|
||||||
className="bg-background"
|
|
||||||
maxLength={100}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange({ page: 1, search: event.target.value })
|
|
||||||
}
|
|
||||||
placeholder="名称或代码"
|
|
||||||
type="search"
|
|
||||||
value={search.search}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user