Files
zhixing-system/zhixing-web/src/features/components/pages/components-preview-page.tsx
T

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>
)
}