287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
|
|
import { format } from "date-fns"
|
||
|
|
import { useState, type ReactNode } from "react"
|
||
|
|
|
||
|
|
import { PageLayout } from "@/app/layout/page-layout"
|
||
|
|
import { Badge } from "@/shared/ui/badge"
|
||
|
|
import { Button } from "@/shared/ui/button"
|
||
|
|
import { Calendar } from "@/shared/ui/calendar"
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
CardDescription,
|
||
|
|
CardHeader,
|
||
|
|
CardTitle,
|
||
|
|
} from "@/shared/ui/card"
|
||
|
|
import { DatePicker } from "@/shared/ui/date-picker"
|
||
|
|
import { Input } from "@/shared/ui/input"
|
||
|
|
import {
|
||
|
|
Popover,
|
||
|
|
PopoverClose,
|
||
|
|
PopoverContent,
|
||
|
|
PopoverTrigger,
|
||
|
|
} from "@/shared/ui/popover"
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectGroup,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/shared/ui/select"
|
||
|
|
import { Separator } from "@/shared/ui/separator"
|
||
|
|
import { Skeleton } from "@/shared/ui/skeleton"
|
||
|
|
|
||
|
|
const signalTypeOptions = [
|
||
|
|
{ label: "全部信号", value: "all" },
|
||
|
|
{ label: "原始 B1", value: "original" },
|
||
|
|
{ label: "超卖类", value: "oversold" },
|
||
|
|
] as const
|
||
|
|
|
||
|
|
export function ComponentsPreviewPage() {
|
||
|
|
const [signalType, setSignalType] = useState("all")
|
||
|
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
||
|
|
() => new Date(2026, 7, 7),
|
||
|
|
)
|
||
|
|
const [calendarDate, setCalendarDate] = useState<Date | undefined>(
|
||
|
|
() => new Date(2026, 7, 7),
|
||
|
|
)
|
||
|
|
const [popoverOpen, setPopoverOpen] = useState(false)
|
||
|
|
const signalTypeLabel =
|
||
|
|
signalTypeOptions.find((option) => option.value === signalType)?.label ??
|
||
|
|
"全部信号"
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PageLayout>
|
||
|
|
<div className="mx-auto w-full max-w-6xl space-y-6">
|
||
|
|
<header className="space-y-2">
|
||
|
|
<p className="text-xs font-medium tracking-[0.12em] text-muted-foreground uppercase">
|
||
|
|
Shared UI
|
||
|
|
</p>
|
||
|
|
<h1 className="text-2xl font-semibold tracking-tight">组件预览</h1>
|
||
|
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||
|
|
集中查看项目 shared/ui 的视觉
|
||
|
|
token、尺寸和交互状态。这里的示例都是真实组件,后续新增组件可以直接在本页补充验证。
|
||
|
|
</p>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
||
|
|
<PreviewCard
|
||
|
|
description="保留项目主题色,同时提供清晰的 hover、focus 和 disabled 状态。"
|
||
|
|
title="Button & Badge"
|
||
|
|
>
|
||
|
|
<div className="flex flex-wrap items-center gap-2">
|
||
|
|
<Button>主要操作</Button>
|
||
|
|
<Button variant="outline">次要操作</Button>
|
||
|
|
<Button variant="secondary">辅助操作</Button>
|
||
|
|
<Button variant="ghost">幽灵按钮</Button>
|
||
|
|
<Button disabled>禁用状态</Button>
|
||
|
|
</div>
|
||
|
|
<Separator className="my-4" />
|
||
|
|
<div className="flex flex-wrap items-center gap-2">
|
||
|
|
<Badge>已完成</Badge>
|
||
|
|
<Badge variant="outline">待确认</Badge>
|
||
|
|
<Badge variant="destructive">失败</Badge>
|
||
|
|
</div>
|
||
|
|
</PreviewCard>
|
||
|
|
|
||
|
|
<PreviewCard
|
||
|
|
description="Select 使用 Base UI 的弹层和键盘语义,字段值由页面状态控制。"
|
||
|
|
title="Select & Input"
|
||
|
|
>
|
||
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium" htmlFor="preview-search">
|
||
|
|
搜索输入
|
||
|
|
</label>
|
||
|
|
<Input id="preview-search" placeholder="搜索股票代码或名称" />
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label
|
||
|
|
className="text-sm font-medium"
|
||
|
|
id="preview-signal-label"
|
||
|
|
>
|
||
|
|
信号分类
|
||
|
|
</label>
|
||
|
|
<Select
|
||
|
|
items={signalTypeOptions}
|
||
|
|
onValueChange={(value) => setSignalType(value ?? "all")}
|
||
|
|
value={signalType}
|
||
|
|
>
|
||
|
|
<SelectTrigger
|
||
|
|
aria-labelledby="preview-signal-label"
|
||
|
|
className="w-full"
|
||
|
|
>
|
||
|
|
<SelectValue placeholder="选择信号分类" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectGroup>
|
||
|
|
{signalTypeOptions.map((option) => (
|
||
|
|
<SelectItem key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectGroup>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p
|
||
|
|
aria-label="当前选择"
|
||
|
|
className="mt-4 text-sm text-muted-foreground"
|
||
|
|
role="status"
|
||
|
|
>
|
||
|
|
当前选择:
|
||
|
|
<span className="font-medium text-foreground">
|
||
|
|
{signalTypeLabel}
|
||
|
|
</span>
|
||
|
|
</p>
|
||
|
|
</PreviewCard>
|
||
|
|
|
||
|
|
<PreviewCard
|
||
|
|
className="lg:col-span-2"
|
||
|
|
description="DatePicker 采用官方示例的文本触发器、右侧下拉指示和 w-auto 日历弹层;主题色仍由项目 token 提供。"
|
||
|
|
title="DatePicker & Calendar"
|
||
|
|
>
|
||
|
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_auto]">
|
||
|
|
<div className="space-y-5">
|
||
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label
|
||
|
|
className="text-sm font-medium"
|
||
|
|
htmlFor="preview-date"
|
||
|
|
>
|
||
|
|
已选择日期
|
||
|
|
</label>
|
||
|
|
<DatePicker
|
||
|
|
aria-label="已选择日期"
|
||
|
|
className="w-full max-w-[212px]"
|
||
|
|
id="preview-date"
|
||
|
|
onChange={setSelectedDate}
|
||
|
|
value={selectedDate}
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
{selectedDate
|
||
|
|
? `值:${format(selectedDate, "yyyy-MM-dd")}`
|
||
|
|
: "尚未选择日期"}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<span className="block text-sm font-medium">
|
||
|
|
空值与禁用态
|
||
|
|
</span>
|
||
|
|
<DatePicker
|
||
|
|
aria-label="空值日期"
|
||
|
|
className="w-full max-w-[212px]"
|
||
|
|
placeholder="选择目标日期"
|
||
|
|
/>
|
||
|
|
<DatePicker
|
||
|
|
aria-label="禁用日期"
|
||
|
|
className="w-full max-w-[212px]"
|
||
|
|
disabled
|
||
|
|
placeholder="日期不可用"
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
className="mt-1"
|
||
|
|
disabled={!selectedDate}
|
||
|
|
onClick={() => setSelectedDate(undefined)}
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
清空已选择日期
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="rounded-lg border border-dashed border-border bg-muted/30 p-3 text-sm text-muted-foreground">
|
||
|
|
交互提示:打开日期弹层后,可以使用方向键移动、Enter
|
||
|
|
选择、Escape 关闭;月份切换按钮与日期按钮都保留可见焦点。
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="rounded-xl border border-border bg-background p-2">
|
||
|
|
<Calendar
|
||
|
|
aria-label="日历组件示例"
|
||
|
|
defaultMonth={calendarDate}
|
||
|
|
mode="single"
|
||
|
|
onSelect={setCalendarDate}
|
||
|
|
selected={calendarDate}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</PreviewCard>
|
||
|
|
|
||
|
|
<PreviewCard
|
||
|
|
description="Popover 负责定位、外部点击关闭、Escape 关闭和触发器焦点恢复。"
|
||
|
|
title="Popover"
|
||
|
|
>
|
||
|
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||
|
|
<PopoverTrigger
|
||
|
|
render={
|
||
|
|
<Button aria-expanded={popoverOpen} variant="outline" />
|
||
|
|
}
|
||
|
|
>
|
||
|
|
打开交互说明
|
||
|
|
</PopoverTrigger>
|
||
|
|
<PopoverContent align="start" className="w-80">
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<p className="text-sm font-medium">弹层已打开</p>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
这里可以承载筛选提示、轻量编辑或上下文信息。
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<PopoverClose render={<Button size="sm" variant="outline" />}>
|
||
|
|
关闭弹层
|
||
|
|
</PopoverClose>
|
||
|
|
</div>
|
||
|
|
</PopoverContent>
|
||
|
|
</Popover>
|
||
|
|
<p
|
||
|
|
aria-label="弹层状态"
|
||
|
|
className="mt-4 text-sm text-muted-foreground"
|
||
|
|
role="status"
|
||
|
|
>
|
||
|
|
当前状态:{popoverOpen ? "已打开" : "已关闭"}
|
||
|
|
</p>
|
||
|
|
</PreviewCard>
|
||
|
|
|
||
|
|
<PreviewCard
|
||
|
|
description="基础容器和反馈状态也在同一处展示,便于发现尺寸和间距不一致。"
|
||
|
|
title="Surface & Feedback"
|
||
|
|
>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-3 py-2 text-sm">
|
||
|
|
<span>数据同步状态</span>
|
||
|
|
<Badge>正常</Badge>
|
||
|
|
</div>
|
||
|
|
<Separator />
|
||
|
|
<Skeleton className="h-4 w-2/3" />
|
||
|
|
<Skeleton className="h-8 w-full" />
|
||
|
|
</div>
|
||
|
|
</PreviewCard>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</PageLayout>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function PreviewCard({
|
||
|
|
children,
|
||
|
|
className,
|
||
|
|
description,
|
||
|
|
title,
|
||
|
|
}: {
|
||
|
|
children: ReactNode
|
||
|
|
className?: string
|
||
|
|
description: string
|
||
|
|
title: string
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<Card className={className}>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">{title}</CardTitle>
|
||
|
|
<CardDescription>{description}</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>{children}</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|