feat(selection): 重构选股结果筛选与状态入口

This commit is contained in:
yuxuanhui
2026-08-10 14:50:56 +08:00
parent 78be0f53d4
commit 8d4f80f2f1
17 changed files with 1123 additions and 156 deletions
+71
View File
@@ -0,0 +1,71 @@
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react"
import { DayPicker, type DayPickerProps } from "react-day-picker"
import { zhCN } from "react-day-picker/locale"
import { cn } from "@/shared/ui/utils"
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: DayPickerProps) {
return (
<DayPicker
className={cn("p-3", className)}
classNames={{
months: "flex flex-col gap-4 sm:flex-row",
month: "flex w-full flex-col gap-4",
month_caption: "flex items-center justify-center pt-1",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
button_previous:
"absolute left-1 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
button_next:
"absolute right-1 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
month_grid: "w-full border-collapse",
weekdays: "flex",
weekday:
"w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground",
week: "mt-2 flex w-full",
day: "relative size-9 p-0 text-center text-sm [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-muted/50 [&:has([aria-selected])]:bg-muted first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day_button:
"inline-flex size-9 items-center justify-center rounded-md p-0 font-normal outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 aria-selected:bg-primary aria-selected:text-primary-foreground",
range_start: "day-range-start rounded-l-md bg-muted",
range_middle: "rounded-none bg-muted",
range_end: "day-range-end rounded-r-md bg-muted",
selected: "rounded-md bg-primary text-primary-foreground",
today: "bg-muted text-foreground",
outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-muted/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
disabled: "text-muted-foreground opacity-50",
hidden: "invisible",
...classNames,
}}
components={{
Chevron: ({ className: chevronClassName, orientation, style }) => {
const Icon =
orientation === "left"
? ChevronLeft
: orientation === "right"
? ChevronRight
: orientation === "up"
? ChevronUp
: ChevronDown
return (
<Icon
aria-hidden="true"
className={cn("size-4", chevronClassName)}
style={style}
/>
)
},
}}
locale={zhCN}
showOutsideDays={showOutsideDays}
{...props}
/>
)
}
export { Calendar }
+62
View File
@@ -0,0 +1,62 @@
import { format } from "date-fns"
import { CalendarDays } from "lucide-react"
import { useState } from "react"
import type { ComponentProps } from "react"
import { Button } from "@/shared/ui/button"
import { Calendar } from "@/shared/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"
export interface DatePickerProps extends Omit<
ComponentProps<typeof Button>,
"children" | "onChange" | "value"
> {
value?: Date
onChange?: (value: Date | undefined) => void
placeholder?: string
}
export function DatePicker({
className,
disabled,
onChange,
placeholder = "选择日期",
value,
...props
}: DatePickerProps) {
const [open, setOpen] = useState(false)
function handleSelect(nextValue: Date | undefined) {
onChange?.(nextValue)
setOpen(false)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
disabled={disabled}
render={
<Button
data-empty={!value}
data-slot="date-picker-trigger"
disabled={disabled}
variant="outline"
className={className}
{...props}
/>
}
>
<CalendarDays aria-hidden="true" data-icon="inline-start" />
{value ? format(value, "yyyy-MM-dd") : <span>{placeholder}</span>}
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
defaultMonth={value}
mode="single"
onSelect={handleSelect}
selected={value}
/>
</PopoverContent>
</Popover>
)
}
+50
View File
@@ -0,0 +1,50 @@
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import { cn } from "@/shared/ui/utils"
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
align = "center",
className,
children,
sideOffset = 4,
...props
}: PopoverPrimitive.Popup.Props & {
align?: PopoverPrimitive.Positioner.Props["align"]
sideOffset?: number
}) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
data-slot="popover-positioner"
align={align}
sideOffset={sideOffset}
className="z-50 outline-none"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"z-50 w-72 origin-(--transform-origin) rounded-lg border border-border bg-card p-4 text-card-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
)
}
function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
}
export { Popover, PopoverClose, PopoverContent, PopoverTrigger }
+200
View File
@@ -0,0 +1,200 @@
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/shared/ui/utils"
function Select<Value, Multiple extends boolean | undefined = false>(
props: SelectPrimitive.Root.Props<Value, Multiple>,
) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("p-1", className)}
{...props}
/>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn(
"px-2 py-1.5 text-xs font-medium text-muted-foreground",
className,
)}
{...props}
/>
)
}
function SelectTrigger({
className,
children,
size = "default",
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-full items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-2.5 text-sm whitespace-nowrap outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 [&>span]:line-clamp-1 [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon
data-slot="select-icon"
className="text-muted-foreground"
>
<ChevronDown aria-hidden="true" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("line-clamp-1", className)}
{...props}
/>
)
}
function SelectContent({
className,
children,
sideOffset = 4,
...props
}: SelectPrimitive.Popup.Props & {
sideOffset?: number
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
data-slot="select-positioner"
sideOffset={sideOffset}
className="z-50 outline-none"
>
<SelectPrimitive.Popup
data-slot="select-content"
className={cn(
"relative z-50 max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List
data-slot="select-list"
className="max-h-72 overflow-y-auto p-1"
>
{children}
</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex min-h-8 w-full cursor-default items-center rounded-md py-1.5 pr-8 pl-2 text-sm outline-none select-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator data-slot="select-item-indicator">
<Check aria-hidden="true" className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText data-slot="select-item-text">
{children}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: SelectPrimitive.ScrollUpArrow.Props) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"flex h-6 items-center justify-center bg-card text-muted-foreground",
className,
)}
{...props}
>
<ChevronUp aria-hidden="true" className="size-4" />
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: SelectPrimitive.ScrollDownArrow.Props) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"flex h-6 items-center justify-center bg-card text-muted-foreground",
className,
)}
{...props}
>
<ChevronDown aria-hidden="true" className="size-4" />
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}