# Yi UI · 文档与博客全文 # AI 助手面板 来源:https://yiui.dev/blocks/agent-chat-01/ 这个 AI 助手面板提炼自我的一个企业监控平台项目,保留「在工作区旁边排查问题」的交互,去掉认证、业务接口与监控数据依赖。默认使用离线模拟数据,不需要密钥或服务端。 - **两种布局**:右侧嵌入面板、右下角浮动窗口;聊天记录、输入草稿和滚动位置在切换时保留。 - **可检查的过程**:分析摘要、运行中的工具、成功与失败结果,以及可打开的参考来源,按顺序排成紧凑的 `Trace`(shadcn 的 `trace`),一行一步;每一步的输入与输出可折叠查看。 - **回答里的图表**:工具返回后,回答上方会出现一张紧凑的图表卡片:可用率走势(`MiniChart` 面积图加指标行)、GPU 占用(三个 `MiniRing` 进度环)、CI 各步骤耗时(`MiniChart` 柱图)。图表以 `data-chart` 数据块的形式在消息里流式出现,宽度跟随面板,高度只有 96px 左右,不会把消息撑长。 - **流式演示**:应用监控、vLLM 显存、CI 安装失败三个剧本,其他问题使用通用剧本。所有 reasoning 均为预写的演示摘要,不是真实模型的隐藏思维。 - **对话操作**:复制、点赞/点踩、重新生成、新建对话、停止生成。反馈仅保存在当前页面,不会上报。 - **窄屏适配**:小于 768px 时,两种模式都使用底部 Sheet,带焦点管理和 Escape 关闭;嵌入面板支持用鼠标或触摸拖动、用方向键调整宽度。 ### 用到的本站组件 除输入框、按钮、气泡、Sheet 等 shadcn 原语外,区块里这几处直接用了本站自己的组件: | 位置 | 组件 | 说明 | | --- | --- | --- | | 回答里的图表 | [`chart-card`](/components/chart-card)、[`mini-chart`](/components/mini-chart) | `ChartCard` 外壳 + `ChartMetrics` 指标行,图形用 `MiniChart`(面积、柱)与 `MiniRing`(进度环) | | 空状态的背景与标题 | [`glyph-field`](/components/glyph-field)、[`text-animate`](/components/text-animate) | 四周淡淡的字符矩阵,欢迎语用 `TextReveal` 逐词显现 | | 示例工作区的布局切换 | [`segmented-control`](/components/segmented-control) | 嵌入布局 / 浮动窗口,选中项就是当前布局 | ## 安装 会安装 `app/agent/page.tsx` 页面,以及 `components/agent-chat/` 下的组件与数据文件。共享 hook 安装到 `hooks/use-controllable-state.ts`。shadcn 依赖来自官方 registry,本站组件 `chart-card`、`mini-chart`(图表及其 `recharts` 依赖)、`glyph-field`、`text-animate`、`segmented-control`(及它们依赖的 `flow-diagram` 色调)一并安装;npm 依赖为 `ai`、`@ai-sdk/react`、`motion`、`lucide-react`、`class-variance-authority`。实现基于 AI SDK 7 与 React SDK 4。 ## 用法 ```tsx title="app/workspace/page.tsx" "use client" import { AgentChatPanel, AgentChatProvider, AgentChatTrigger, } from "@/components/agent-chat/agent-chat" export default function Workspace() { return (
你的工作区
) } ``` Provider 内只创建一个 `useChat` 实例,也只挂载一个面板。将面板放在与 `main` 同级的 flex 容器中;嵌入时默认宽 400px,调整范围 320–640px,父容器需要有明确高度与 `min-h-0`。 ## 示例 ### 受控布局与自定义文案 ```tsx title="components/workspace.tsx" "use client" import { useState } from "react" import { AgentChatPanel, AgentChatProvider, AgentChatTrigger, type AgentChatMode, } from "@/components/agent-chat/agent-chat" export function Workspace() { const [mode, setMode] = useState("embedded") const [open, setOpen] = useState(true) return (
你的工作区
) } ``` 预览页包含嵌入、浮动、预填记录和从工作区发起问题的组合示例。 ## API ### AgentChatProvider ", description: "聊天传输层;不传则使用内置 mock transport" }, suggestions: { type: "string[]", description: "建议问题;不传时显示三个示例问题,传 `[]` 隐藏" }, title: { type: "string", description: "面板标题", default: '"AI 助手"' }, initialMessages: { type: "UIMessage[]", description: "可选的初始记录,演示页用它预填一轮对话" }, children: { type: "ReactNode", description: "工作区内容", required: true }, }} /> `default*` 与 `initialMessages` 只用于初始化。更换会话或 transport 配置时,可通过 Provider 的 React `key` 重新创建实例。会话状态仅在当前 Provider 生命周期内保存,刷新后重置;浮动窗口位置单独保存在 `localStorage`(`travinsy:agent-chat:position`),刷新或切换布局后仍会恢复。读取时以及窗口尺寸变化时,都会把位置约束在视口内。存储不可用时仍可在当前页面移动。 ### labels `labels` 接收 `Partial`,覆盖 `config.ts` 中 `DEFAULT_LABELS` 的任意几项(面板标题、输入框占位、按钮名称、状态文案等),例如 `labels={{ title: "工程助手", welcome: "今天要解决什么问题?" }}`。`thought` 支持 `{seconds}` 占位符;`moveWindow`(默认 `"移动助手窗口"`)用作浮动窗口拖动手柄的可访问名称;完整键列表见 `config.ts`。 ### AgentChatPanel `draggable?: boolean` 默认 `true`,允许桌面浮动模式拖动;传 `false` 禁用指针和键盘移动。小于 768px 的 Sheet 自动禁用拖动。标题栏支持鼠标与触控拖动,操作按钮不会触发拖动;双击标题栏复位到默认右下角。`data-draggable` 表示可移动,拖动期间面板与标题栏设置 `data-dragging="true"`。 ### 触发器、建议问题与 Hook | 导出 | API | | --------------------- | -------------------------------------------------------------------------------------------------------------- | | `AgentChatPanel` | `{ className?: string; id?: string; draggable?: boolean }`;关闭时不渲染;桌面嵌入为 `aside` | | `AgentChatTrigger` | `{ variant?: "fab" \| "button"; className?: string }`;默认 `fab`,只在浮动模式关闭时显示 | | `useAgentChat()` | `{ mode, setMode, toggleMode, open, setOpen }`;必须在 Provider 内调用 | | `AgentChatSuggestion` | `{ question: string; children?: ReactNode; className?: string }`;从外部工作区打开面板并发送问题,生成期间禁用 | | `AgentChatMode` | `"embedded" \| "floating"` | `Enter` 发送,`Shift + Enter` 换行,中文输入法选词时不会误发送。生成中点击「停止」调用 `stop()`;已收到的内容保留,未完成的工具标记为「已停止」。阅读旧消息时不会强制滚动到底部。 ## 可访问性 - 标题栏与反馈按钮均有中文可访问名称,输入框有标签。 - 分析过程里每一步的状态写在标题文字里(运行中 / 已完成 / 失败 / 已停止),图标仅装饰;展开按钮暴露 `aria-expanded`。 - 图表卡片里的图是 `role="img"`,名称来自剧本里的 `label`;进度环是 `role="meter"`;关键数字同时写在指标行或环中央的文字里,不只靠图形。 - 聊天记录通过 `aria-live="polite"` 播报新增内容;按钮暴露 `aria-expanded`、`aria-pressed`,面板通过 `data-mode`、`data-state` 暴露状态。 - `Enter` 发送,`Shift + Enter` 换行;输入法组合期间不会误发。调整面板宽度可用左右方向键。 - 浮动模式下,Tab 聚焦「移动助手窗口」手柄,方向键每次移动 16px,Shift + 方向键移动 64px;Home、Enter 或空格复位到默认右下角。双击标题栏也可复位。移动始终限制在视口内;窄屏或 `draggable={false}` 时不显示手柄。 - 窄屏 Sheet 提供焦点管理、焦点回到触发区域和 Escape 关闭。 - 动效遵循 `prefers-reduced-motion`,来源链接标注新标签页。 ## 注意事项 ### 文件结构 ```text components/agent-chat/ ├── agent-chat.tsx # 对外入口、面板和触发器 ├── agent-chat-context.tsx # Provider 与会话状态 ├── use-agent-chat-position.ts # 指针/键盘移动、视口约束与位置持久化 ├── agent-chat-body.tsx # 标题栏、滚动区、输入框 ├── chat-message.tsx # 消息气泡、分析过程 Trace、回复正文与反馈 ├── chat-charts.tsx # 回答里的图表卡片(迷你图、进度环) ├── chat-markdown.tsx # 安全的简单 Markdown ├── config.ts # DEFAULT_LABELS 与建议问题 ├── types.ts # 模式类型与图表块类型 ChatChart ├── mock-transport.ts # 可取消的流 ├── scripts.ts # 预置剧本 └── demo-data.ts # 页面默认记录与场景 hooks/use-controllable-state.ts app/agent/page.tsx ``` ### Mock transport 设计 `mock-transport.ts` 按最后一条用户消息匹配正则,构造标准 `UIMessageChunk` 队列。每次 `pull` 只输出一个片段:reasoning → 多个 dynamic tool → data-chart(图表)→ source-url → text → finish。延迟定时器受 `AbortSignal` 和流取消控制,不会执行真实命令或请求业务服务;`reconnectToStream` 返回 `null`。 `chat-message.tsx` 同时接受动态工具与 `tool-*` 静态工具消息。图表块的结构见 `types.ts` 的 `ChatChart`(`series` 走势 / 柱图、`rings` 进度环),必须可被 JSON 序列化,真实后端用 `type: "data-chart"` 的数据块下发即可。Markdown 仅支持段落、列表与行内代码,不执行 HTML;来源 URL 仅允许 HTTP(S)。 ### 接入真实后端 客户端只需替换 transport;保持 `useChat` 在 Provider 中: ```tsx title="app/workspace/page.tsx" "use client" import { useState } from "react" import { DefaultChatTransport } from "ai" import { AgentChatPanel, AgentChatProvider, AgentChatTrigger, } from "@/components/agent-chat/agent-chat" export default function Workspace() { const [transport] = useState( () => new DefaultChatTransport({ api: "/api/chat" }), ) return (
你的工作区
) } ``` 下面的服务端示例应放在支持服务端运行的应用或独立 API 服务中。**本站是纯静态导出,不部署这个 POST Route Handler。** 跨域后端可以给 `api` 配置完整 URL,并在后端配置 CORS。真实应用还需实现认证、输入校验与工具权限控制。 ```tsx title="app/api/chat/route.ts(服务端应用示例)" import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage, } from "ai" import { z } from "zod" export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json() const result = streamText({ // 使用你配置的模型或 AI Gateway;密钥仅在服务端配置。 model: "openai/gpt-5-mini", messages: await convertToModelMessages(messages), abortSignal: request.signal, stopWhen: stepCountIs(3), tools: { readMetrics: tool({ description: "读取授权范围内的监控摘要", inputSchema: z.object({ service: z.string() }), execute: async ({ service }) => { // 替换为带权限检查的真实只读查询。 return { service, availability: "99.95%", demo: true } }, }), }, }) return result.toUIMessageStreamResponse({ originalMessages: messages, sendReasoning: true, sendSources: true, }) } ``` `sendReasoning` 仅在模型提供可公开的 reasoning 内容时才有数据;不支持时可以省略该部分。工具 `input-streaming / input-available` 映射为运行中,`output-available` 映射为完成,`output-error / output-denied` 映射为失败;停止时未结束的调用显示为已停止。模型或检索工具提供的 `source-url` 映射为来源链接,不会自动生成引用。服务端应只发送适合向用户公开的分析摘要与工具输出。 参考:[AI SDK UI](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot)。 --- # 分屏登录页 来源:https://yiui.dev/blocks/auth-01/ `Auth01` 提炼自我的一个企业监控平台项目的登录页,在原有「左侧品牌面板 + 右侧表单」结构上,补上了完整的账号流程: - **登录**:密码登录 / 验证码登录用方条分段控件切换;账号(用户名或邮箱)、密码显示切换、记住登录状态开关、忘记密码、第三方登录(GitHub、Google)、跳转注册。 - **注册**:姓名、邮箱、密码(实时强度条)、确认密码、同意服务条款。 - **找回密码**:邮箱提交后切换为「邮件已发送」的成功状态,邮件图标向外扩散波纹。 - 三个视图在同一个组件内切换,带 180ms 的淡入位移过渡,系统开启「减少动态效果」时只保留淡入。 - 大屏左半屏是品牌面板,窄屏隐藏面板并在表单上方显示 Logo 与名称。 - 品牌面板的颜色都来自主题变量,切换颜色主题和亮暗模式后自动跟随:亮色下是 `primary` 底配 `primary-foreground` 文字;暗色下 `primary` 会变成浅色,面板改用 `card` 深底,只叠一层低透明度的 `primary` 渐变染上主题色,文字回到 `foreground`。光斑、波纹、字符矩阵共用一个 `--panel-glow` 变量。预览里可以直接试。 - 输入框、提交按钮、第三方登录按钮、「获取验证码」、「返回登录」统一为 36px(按钮用 `size="lg"`,输入框 `h-9`),与方条分段控件的 `lg` 档对齐。 ### 用到的本站组件 这个区块本身就是自研组件的一组真实用例,除表单输入、按钮、复选框(注册页的条款勾选)仍用 shadcn 原语外,其余都换成了本站组件: | 位置 | 组件 | 说明 | | --- | --- | --- | | 品牌面板背景 | [`ripple`](/components/ripple) | 以面板中心向外扩散的同心波纹,替代原来的静态圆环 | | 品牌面板四周 | [`glyph-field`](/components/glyph-field) | 淡淡的 `0 / 1` 字符矩阵,`fade="center"` 让中间留白给文字 | | 品牌面板标题 | [`text-animate`](/components/text-animate) 的 `TextReveal` | 挂载时按词逐个显现,读屏仍读完整标题 | | 登录方式切换 | [`segmented-control`](/components/segmented-control) | 密码登录 / 验证码登录,替代原来的 Tabs | | 记住登录状态 | [`toggle-switch`](/components/toggle-switch) | 即时生效的设置项,替代原来的复选框 | | 「邮件已发送」 | [`ripple`](/components/ripple) | 邮件图标为圆心,表达「已发出」 | ## 安装 会安装 shadcn 的 `button`、`input`、`label`、`checkbox`,本站的 `ripple`、`glyph-field`、`text-animate`、`segmented-control`、`toggle-switch`(及它们依赖的 `flow-diagram` 色调与背景),以及 npm 包 `motion`、`zod`、`lucide-react`。如果项目里已有同名文件,CLI 会询问是否覆盖。 ## 用法 最简单的用法是直接渲染,不传任何属性。此时所有回调都退回「控制台打印 + 模拟 0.8 秒请求」的示例实现,方便先看效果: ```tsx title="app/login/page.tsx" import { Auth01 } from "@/components/auth-01/auth-01" export default function LoginPage() { return } ``` ### 接入你的认证服务 回调都返回 `Promise`。**组件负责按钮的 loading 状态;失败时 `throw new Error("中文原因")`,错误信息会显示在表单上方。** 下面以 better-auth 为例,NextAuth / Auth.js、Supabase 等同理: ```tsx title="app/login/page.tsx" "use client" import { useRouter } from "next/navigation" import { Auth01 } from "@/components/auth-01/auth-01" import { authClient } from "@/lib/auth-client" export default function LoginPage() { const router = useRouter() return ( { if (values.method === "code") { // 验证码登录:values.phone / values.code throw new Error("暂未开通验证码登录") } const isEmail = values.account.includes("@") const { error } = isEmail ? await authClient.signIn.email({ email: values.account, password: values.password, rememberMe: values.remember, }) : await authClient.signIn.username({ username: values.account, password: values.password, rememberMe: values.remember, }) if (error) throw new Error(error.message ?? "登录失败") router.push("/") }} onSignUp={async ({ name, email, password }) => { const { error } = await authClient.signUp.email({ name, email, password }) if (error) throw new Error(error.message ?? "注册失败") router.push("/") }} onForgotPassword={async (email) => { const { error } = await authClient.requestPasswordReset({ email }) if (error) throw new Error(error.message ?? "发送失败") }} onSendCode={async (phone) => { await fetch("/api/sms/send", { method: "POST", body: JSON.stringify({ phone }) }) }} onSocialSignIn={async (provider) => { await authClient.signIn.social({ provider }) }} /> ) } ``` 使用 NextAuth 时,把 `onSignIn` 换成 `signIn("credentials", { redirect: false, ... })` 并在返回 `error` 时抛出即可;第三方登录对应 `signIn(provider)`。 ### 换成英文 `labels` 也能覆盖校验提示(`validation*` 键)。提示里的 `{min}`、`{length}` 由 `schemas.ts` 的规则常量填入,**保留占位符**就会显示成对应的数字: ```tsx title="app/login/page.tsx" import { Auth01, type Auth01Labels } from "@/components/auth-01/auth-01" // 只覆盖需要改的键,没写的键仍是默认中文 const english: Partial = { signInTitle: "Welcome back", signInDescription: "Sign in to continue", accountLabel: "Username or email", accountPlaceholder: "you@example.com", passwordLabel: "Password", passwordPlaceholder: "Enter your password", signInSubmit: "Sign in", signUpTitle: "Create an account", nameLabel: "Name", emailLabel: "Email", newPasswordPlaceholder: "At least 8 characters, letters and numbers", confirmPasswordLabel: "Confirm password", signUpSubmit: "Sign up", // 校验提示:{min}、{length} 一定要保留,否则数字不会被填入 validationAccountRequired: "Enter your username or email", validationPasswordRequired: "Enter your password", validationEmailRequired: "Enter your email", validationEmailInvalid: "Enter a valid email address", validationPhoneInvalid: "Enter a valid {length}-digit phone number", validationCodeInvalid: "Enter the {length}-digit code", validationPasswordMin: "Use at least {min} characters", validationPasswordLetter: "Include a letter", validationPasswordDigit: "Include a number", validationNameMin: "Name needs at least {min} characters", validationConfirmRequired: "Re-enter your password", validationPasswordMismatch: "Passwords do not match", validationTermsRequired: "Please accept the terms to continue", } export default function LoginPage() { return } ``` 完整键与默认值见下方 [labels](#labels)。 ## API ### Auth01 ", description: "品牌信息,逐项覆盖默认示例品牌,见下方 BrandConfig" }, defaultView: { type: '"sign-in" | "sign-up" | "forgot-password"', description: "初始视图", default: '"sign-in"' }, socialProviders: { type: '("github" | "google")[]', description: "启用的第三方登录,传 `[]` 隐藏该区域;默认两个都开", default: '["github", "google"]' }, allowSignUp: { type: "boolean", description: "设为 `false` 隐藏「注册」入口", default: "true" }, termsHref: { type: "string", description: "注册页中「服务条款」的链接" }, privacyHref: { type: "string", description: "注册页中「隐私政策」的链接" }, labels: { type: "Partial", description: "覆盖默认文案,键与默认值见下方 [labels](#labels)" }, onSignIn: { type: "(values: SignInValues) => Promise", description: '`values` 是联合类型:`{ method: "password", account, password, remember }` 或 `{ method: "code", phone, code }`' }, onSignUp: { type: "(values: { name; email; password }) => Promise", description: "校验通过后才会调用;确认密码与条款勾选已在组件内校验" }, onForgotPassword: { type: "(email: string) => Promise", description: "成功返回后显示「邮件已发送」" }, onSendCode: { type: "(phone: string) => Promise", description: "成功返回后「获取验证码」按钮进入 60 秒倒计时" }, onSocialSignIn: { type: "(provider) => Promise", description: "点击第三方登录按钮时调用" }, className: { type: "string", description: "追加到最外层容器" }, }} /> ### BrandConfig `", required: true }, title: { type: "string", description: "品牌面板的大标题", required: true }, description: { type: "string", description: "品牌面板的说明", required: true }, features: { type: "{ icon: ReactNode; title; description }[]", description: "特性列表,建议 2–3 条,图标用 `lucide-react`", required: true }, copyright: { type: "string", description: "右下角版权文字", required: true }, }} /> ### labels 所有键都在 `config.ts` 的 `DEFAULT_LABELS` 里,传 `labels={{ signInTitle: "Sign in" }}` 这样覆盖其中任意几项。含 `{name}`、`{email}`、`{seconds}`、`{level}` 的文案会在渲染时填入实际值;表格末尾的 `validation*` 键是表单校验提示,其中的 `{min}`、`{length}` 由 `schemas.ts` 的规则常量填入(例如改小密码最小长度后,提示里的数字会自动变)。 | 键 | 默认值 | 说明 | |---|---|---| | `brandPanelLabel` | `{name} 品牌介绍` | 品牌面板的 `aria-label` | | `signInTitle` | `欢迎回来` | 登录视图标题(`h1`) | | `signInDescription` | `输入账号信息即可登录` | 登录视图说明 | | `signInMethodLabel` | `登录方式` | 登录方式分段控件的 `aria-label` | | `signInPasswordTab` | `密码登录` | 密码登录选项 | | `signInCodeTab` | `验证码登录` | 验证码登录选项 | | `accountLabel` | `用户名或邮箱` | 账号输入框的标签 | | `accountPlaceholder` | `请输入用户名或邮箱` | 账号输入框的占位文字 | | `passwordLabel` | `密码` | 密码输入框的标签 | | `passwordPlaceholder` | `请输入密码` | 密码输入框的占位文字 | | `forgotPassword` | `忘记密码?` | 密码框右侧的链接按钮 | | `remember` | `记住登录状态` | 记住登录状态开关的标签 | | `signInSubmit` | `登录` | 登录按钮与其加载文案 | | `signInPending` | `登录中…` | 登录按钮与其加载文案 | | `phoneLabel` | `手机号` | 验证码登录的手机号输入框的标签 | | `phonePlaceholder` | `请输入 11 位手机号` | 验证码登录的手机号输入框的占位文字 | | `codeLabel` | `验证码` | 验证码输入框的标签 | | `codePlaceholder` | `6 位数字验证码` | 验证码输入框的占位文字 | | `sendCode` | `获取验证码` | 「获取验证码」按钮的三种状态 | | `sendCodePending` | `发送中…` | 「获取验证码」按钮的三种状态 | | `resendCode` | `{seconds}s 后重发` | 「获取验证码」按钮的三种状态 | | `socialDivider` | `或` | 第三方登录上方的分隔文字 | | `noAccount` | `还没有账号?` | 跳转注册 | | `signUpLink` | `注册` | 跳转注册 | | `signUpTitle` | `创建账号` | 注册视图标题(`h1`) | | `signUpDescription` | `填写以下信息即可完成注册` | 注册视图说明 | | `nameLabel` | `姓名` | 姓名输入框的标签 | | `namePlaceholder` | `请输入姓名` | 姓名输入框的占位文字 | | `emailLabel` | `邮箱` | 邮箱输入框(注册与找回密码共用) | | `emailPlaceholder` | `name@example.com` | 邮箱输入框(注册与找回密码共用) | | `newPasswordPlaceholder` | `至少 8 位,包含字母和数字` | 注册密码框占位文字 | | `confirmPasswordLabel` | `确认密码` | 确认密码输入框的标签 | | `confirmPasswordPlaceholder` | `再次输入密码` | 确认密码输入框的占位文字 | | `termsPrefix` | `我已阅读并同意` | 条款勾选行的四段文字 | | `termsOfService` | `服务条款` | 条款勾选行的四段文字 | | `termsConjunction` | `和` | 条款勾选行的四段文字 | | `privacyPolicy` | `隐私政策` | 条款勾选行的四段文字 | | `signUpSubmit` | `注册` | 注册按钮与其加载文案 | | `signUpPending` | `注册中…` | 注册按钮与其加载文案 | | `haveAccount` | `已有账号?` | 跳转登录 | | `signInLink` | `登录` | 跳转登录 | | `forgotPasswordTitle` | `找回密码` | 找回密码视图标题(`h1`) | | `forgotPasswordDescription` | `输入注册时使用的邮箱,我们会发送重置链接` | 找回密码视图说明 | | `sendResetLink` | `发送重置链接` | 提交按钮与其加载文案 | | `sendResetLinkPending` | `发送中…` | 提交按钮与其加载文案 | | `backToSignIn` | `返回登录` | 返回登录按钮 | | `emailSentTitle` | `邮件已发送` | 发送成功视图标题(`h1`) | | `emailSentDescription` | `我们已向 {email} 发送了重置密码的链接,请查收邮件。没收到?请检查垃圾邮件箱。` | 发送成功视图说明,`{email}` 会加粗显示 | | `showPassword` | `显示密码` | 密码显示切换按钮的 `aria-label` | | `hidePassword` | `隐藏密码` | 密码显示切换按钮的 `aria-label` | | `passwordStrength` | `强度:{level}` | 强度条右侧文字 | | `passwordStrengthEmpty` | `强度:—` | 强度条右侧文字 | | `strengthWeak` | `弱` | 四个强度等级 | | `strengthFair` | `一般` | 四个强度等级 | | `strengthGood` | `较强` | 四个强度等级 | | `strengthStrong` | `强` | 四个强度等级 | | `validationAccountRequired` | `请输入用户名或邮箱` | 密码登录:账号为空 | | `validationPasswordRequired` | `请输入密码` | 密码登录:密码为空 | | `validationEmailRequired` | `请输入邮箱` | 邮箱为空(注册、找回密码) | | `validationEmailInvalid` | `请输入有效的邮箱地址` | 邮箱格式错误 | | `validationPhoneInvalid` | `请输入有效的 {length} 位手机号` | 验证码登录:手机号格式错误,`{length}` 默认 `11` | | `validationCodeInvalid` | `请输入 {length} 位数字验证码` | 验证码格式错误,`{length}` 默认 `6` | | `validationPasswordMin` | `密码至少 {min} 位` | 密码太短,`{min}` 默认 `8` | | `validationPasswordLetter` | `密码需要包含字母` | 密码缺少字母 | | `validationPasswordDigit` | `密码需要包含数字` | 密码缺少数字 | | `validationNameMin` | `姓名至少 {min} 个字符` | 姓名太短,`{min}` 默认 `2` | | `validationConfirmRequired` | `请再次输入密码` | 确认密码为空 | | `validationPasswordMismatch` | `两次输入的密码不一致` | 两次输入的密码不同 | | `validationTermsRequired` | `请先阅读并同意服务条款` | 未勾选条款 | | `requestFailed` | `操作失败,请稍后重试` | 回调抛出的错误没有 `message` 时的兜底提示 | ## 可访问性 - 每个视图只有一个 `h1`(由 `ViewHeading` 渲染),品牌面板是带 `aria-label` 的 `aside`,光斑、波纹与字符矩阵等装饰全部 `aria-hidden`;品牌标题的逐词动画只作用在隐藏层,读屏读到的是完整原文。 - 输入框与 `Label` 通过 `htmlFor` / `id` 关联;字段错误用 `aria-describedby` 关联到同一个输入框,出错时设置 `aria-invalid`;表单级错误是 `role="alert"`,出现时立即朗读。 - 密码显示切换按钮带 `aria-pressed` 和随状态变化的中文名称(显示密码 / 隐藏密码)。 - 登录方式是带 `aria-label` 的单选组(`segmented-control`),方向键切换;「记住登录状态」是 `role="switch"` 的开关;「或」分隔线是 `role="separator"`。 - 密码强度条是装饰(`aria-hidden`),文字结果用 `aria-live="polite"` 播报;强度不只靠颜色区分,同时有文字。 - 「邮件已发送」视图是 `role="status"`,读屏会读出提交结果。 - 提交中按钮禁用并显示「登录中…」,避免重复提交;所有图标按钮都有中文 `aria-label`。 - 视图切换动画遵循 `prefers-reduced-motion`,开启后只保留淡入;波纹、字符矩阵与标题显现在开启「减少动态效果」时显示静态画面,标题直接显示全文。 ## 注意事项 ### 文件结构 ```text app/login/page.tsx # 页面入口:把回调接到你的认证服务 components/auth-01/ ├── auth-01.tsx # 主组件:分屏布局 + 视图切换 ├── config.ts # ★ 全部用户可见文案 DEFAULT_LABELS ├── brand.tsx # 品牌配置类型与默认示例品牌 ├── brand-panel.tsx # 左侧品牌面板(波纹 + 字符矩阵 + 标题显现)、窄屏顶部品牌 ├── sign-in-form.tsx # 登录(分段控件切换密码 / 验证码)+ 第三方登录 ├── sign-up-form.tsx # 注册 ├── forgot-password-form.tsx # 找回密码(成功态用波纹) ├── password-strength.tsx # 密码强度条与评分 ├── social-buttons.tsx # GitHub / Google 按钮 ├── form-parts.tsx # 输入框、密码框、错误提示等共用件 ├── schemas.ts # zod 校验规则工厂与规则常量,提示语取自 labels └── use-auth-form.ts # 受控表单状态 + 校验 + 提交状态 ``` - **校验规则与校验提示分开**:规则常量(`PASSWORD_MIN_LENGTH`、`NAME_MIN_LENGTH`、`CODE_LENGTH`、`PHONE_PATTERN` / `PHONE_LENGTH`)在 `schemas.ts`,提示文案是 `labels` 的 `validation*` 键;提示里的 `{min}`、`{length}` 由常量填入,改规则后文案里的数字自动更新。schema 由 `createSignInSchema(labels)` 这类工厂函数生成,组件里用 `useMemo` 缓存,`labels` 引用不变就不会重建。 - **前端校验不是安全边界**:手机号、密码策略只是体验层,服务端必须再校验一次。 - **静态导出**:组件是纯客户端组件,不使用 Server Actions 或 `cookies()`,可用于 `output: "export"`。 ### 如何自定义 - **校验规则**:规则常量都在 `schemas.ts`。手机号默认是中国大陆 11 位规则,面向其他地区时替换 `PHONE_PATTERN` 并同步 `PHONE_LENGTH`;密码策略是 `PASSWORD_MIN_LENGTH` 与两条正则(必须含字母、必须含数字);姓名最短长度是 `NAME_MIN_LENGTH`,验证码位数是 `CODE_LENGTH`。提示文案不用改,数字会跟着常量走;`newPasswordPlaceholder`(「至少 8 位,包含字母和数字」)是静态示例文案,改了最小长度记得一并改。 - **强度条算法**:`password-strength.tsx` 的 `getPasswordStrength`,按长度(8、12)与大小写、数字、符号计分,颜色从 `destructive` 过渡到 `primary`。 - **只要登录**:传 `allowSignUp={false}`,再传 `socialProviders={[]}` 隐藏第三方登录(省略时默认会显示 GitHub 和 Google)。 - **增减第三方平台**:在 `social-buttons.tsx` 的 `providers` 里补一项,并扩展 `SocialProvider` 类型。 - **改文案**:优先用 `labels`,校验提示也一样,见上方[换成英文](#换成英文);整站换语言时也可以直接改 `config.ts` 的 `DEFAULT_LABELS`。 - **表单状态**:刻意只用受控状态 + zod,没有引入 react-hook-form。字段首次失焦时校验该字段,提交失败后改为输入时实时校验。 --- # 区块 来源:https://yiui.dev/blocks/ --- # 设置页 来源:https://yiui.dev/blocks/settings-01/ `Settings01` 是「方条控件」系列的落地样板:数值滑块、分段选择、开关、色调选择和控制面板拼在一起,就是一个能直接安装使用的设置页。 - **五个分区**:外观、通知、声音与触感、编辑器、隐私。宽屏是左侧导航,导航项右侧的小圆标显示该分区已修改的项数;窄屏(< 768px)导航变成顶部的分段选择。 - **外观**:主题模式(浅色 / 深色 / 跟随系统,带图标)、强调色、界面字号(可直接输入)、圆角、信息密度、减少动态效果。右上角的预览卡片随这些设置实时变化。强调色还会染到整页控件的选中态上。 - **通知**:总开关关闭时其余开关和免打扰时段一起禁用;免打扰是一个区间滑块,读数写成「22:00 – 07:00」。 - **声音与触感**:主音量滑块拖动时有刻度音效;按键音效关闭时音色选择禁用。 - **编辑器**:缩进宽度、行高、制表符显示,右侧的代码预览同步变化。 - **隐私**:资料可见范围、数据收集开关、历史保留天数(刻度吸附 30 / 90 / 180 天)。 - **已修改**:每个控件的 `modified` 由「当前值 ≠ 默认值」得出,改过的项标签变亮;每个分区的面板头部显示本分区已修改项数,并带「复制设置」和「恢复本分区默认值」两个按钮。 - **底部保存栏**:显示总的已修改项数与保存状态,右侧是「恢复默认」和「保存」。保存时按钮进入 loading,成功显示「已保存」,失败显示回调抛出的 `Error.message`。 ## 安装 会安装 `app/settings/page.tsx` 页面和 `components/settings-01/` 下的组件,并带上它依赖的本站控件:数值滑块、分段选择、开关、色调选择、控制面板(连同 `flow-diagram` 提供的色调变量)。shadcn 依赖为 `button`,npm 依赖为 `motion`、`lucide-react`。 ## 用法 最简单的用法是直接渲染,不传任何属性。此时设置值在组件内部维护,「保存」退回「控制台打印 + 模拟 0.8 秒请求」的示例实现,方便先看效果: ```tsx title="app/settings/page.tsx" import { Settings01 } from "@/components/settings-01/settings-01" export default function SettingsPage() { return } ``` ### 接入保存 `onSave` 返回 `Promise`。**组件负责按钮的 loading 状态;失败时 `throw new Error("中文原因")`,原因会显示在保存栏上。** 已有的设置用 `defaultValue`(或受控的 `value`)传入,没传的项取默认值: ```tsx title="app/settings/page.tsx" "use client" import { Settings01, type Settings01Values, } from "@/components/settings-01/settings-01" export default function SettingsPage({ saved, }: { saved: Partial }) { return ( { const response = await fetch("/api/settings", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(values), }) if (!response.ok) throw new Error("保存失败,请稍后重试") }} /> ) } ``` ### 受控用法与实时生效 `value` / `onValueChange` 让你把设置放进自己的状态里,例如切换主题时立刻生效,不必等点「保存」: ```tsx title="app/settings/page.tsx" "use client" import { useState } from "react" import { useTheme } from "next-themes" import { Settings01, type Settings01Values, } from "@/components/settings-01/settings-01" import { DEFAULT_VALUES } from "@/components/settings-01/config" export default function SettingsPage() { const { setTheme } = useTheme() const [values, setValues] = useState(DEFAULT_VALUES) return ( { if (next.theme !== values.theme) setTheme(next.theme) setValues(next) }} onSave={async (next) => { await saveToServer(next) }} /> ) } ``` ### 自定义默认值 `defaults` 逐项覆盖出厂默认值。「已修改」「恢复默认」都以它为准,没有传 `defaultValue` 时它也是初始值: ```tsx ``` ### 换成英文 `labels` 覆盖任意文案,含 `{count}`、`{value}` 的占位符要保留: ```tsx import { Settings01, type Settings01Labels } from "@/components/settings-01/settings-01" const english: Partial = { title: "Settings", sectionAppearance: "Appearance", navAppearance: "Look", save: "Save", resetAll: "Reset to defaults", modifiedCount: "{count} changed", readoutPx: "{value} px", } export default function SettingsPage() { return } ``` ## API ### Settings01 ", description: "非受控时的初始值,没写的项取 `defaults`" }, defaults: { type: "Partial", description: "出厂默认值,逐项覆盖 `config.ts` 的 `DEFAULT_VALUES`;决定「已修改」的判断和「恢复默认」的目标" }, onValueChange: { type: "(value: Settings01Values) => void", description: "任何一项变化时回调,参数是变化后的全部设置(受控与非受控都会触发)" }, section: { type: "Settings01SectionId", description: "受控的当前分区" }, defaultSection: { type: '"appearance" | "notifications" | "sound" | "editor" | "privacy"', description: "非受控时的初始分区", default: '"appearance"' }, onSectionChange: { type: "(section: Settings01SectionId) => void", description: "切换分区时回调" }, onSave: { type: "(values: Settings01Values) => Promise", description: "点击「保存」时调用;组件管理 loading,抛出的 `Error.message` 显示在保存栏。未传时只在控制台打印并等待 0.8 秒" }, labels: { type: "Partial", description: "覆盖默认文案,键与默认值见下方 [labels](#labels)" }, className: { type: "string", description: "追加到最外层容器" }, }} /> ### Settings01Values `config.ts` 的 `SECTION_KEYS` 记录了每个分区管哪些键。 ### labels 所有键都在 `config.ts` 的 `DEFAULT_LABELS` 里,传 `labels={{ save: "Save" }}` 这样覆盖其中任意几项。含 `{count}`、`{value}` 的文案会在渲染时填入实际值。 | 键 | 默认值 | 说明 | |---|---|---| | `title` / `description` | `设置` / `调整外观、通知、声音与编辑器的偏好` | 页面标题(`h1`)与说明 | | `navLabel` | `设置分区` | 导航与顶部分段选择的 `aria-label` | | `navSectionModified` | `已修改 {count} 项` | 导航项圆标的提示与读屏文字 | | `sectionAppearance` 等 5 个 | `外观` / `通知` / `声音与触感` / `编辑器` / `隐私` | 分区标题(`h2`) | | `navAppearance` 等 5 个 | `外观` / `通知` / `声音` / `编辑器` / `隐私` | 窄屏顶部分段选择里的短名 | | `descriptionAppearance` 等 5 个 | 见 `config.ts` | 分区说明 | | `panelModified` / `panelDefault` | `已修改 {count} 项` / `全部为默认值` | 面板头部的状态 | | `copySection` / `copiedSection` / `resetSection` | `复制本分区设置` / `已复制` / `恢复本分区默认值` | 面板头部两个图标按钮的名称 | | `group*` | 见 `config.ts` | 各面板里的分组小标题 | | `theme`、`themeLight`、`themeDark`、`themeSystem` | `主题模式`、`浅色`、`深色`、`跟随系统` | 主题模式 | | `accent`、`fontSize`、`radius`、`density`、`densityCompact`、`densityComfortable`、`densitySpacious`、`reduceMotion`、`reduceMotionHint` | 见 `config.ts` | 外观其余控件 | | `readoutPx` / `readoutDays` | `{value} px` / `{value} 天` | 字号、圆角、保留天数的读数 | | `preview*` | 见 `config.ts` | 外观预览卡片里的文字 | | `notifications`、`notify*`、`quietHours` 及对应 `*Hint` | 见 `config.ts` | 通知控件与它们的补充说明 | | `volume`、`keySound`、`soundTheme`、`sound*`、`haptics` 及对应 `*Hint` | 见 `config.ts` | 声音与触感控件 | | `indent`、`indentTwo`、`indentFour`、`indentTab`、`lineHeight`、`showTabs`、`showTabsHint`、`editorPreviewTitle` | 见 `config.ts` | 编辑器控件与预览标题 | | `visibility`、`visibility*`、`analytics`、`crashReports`、`retention` 及对应 `*Hint` | 见 `config.ts` | 隐私控件 | | `footerLabel` | `保存设置` | 底部保存栏的 `aria-label` | | `modifiedCount` / `unmodified` | `已修改 {count} 项` / `全部为默认值` | 保存栏左侧的总计 | | `unsaved` / `upToDate` / `saving` / `saved` | `有未保存的更改` / `没有未保存的更改` / `保存中…` / `已保存` | 保存栏左侧第二行的保存状态 | | `save` / `resetAll` | `保存` / `恢复默认` | 保存栏的两个按钮 | | `saveFailed` | `保存失败,请稍后重试` | 回调抛出的错误没有 `message` 时的兜底提示 | ## 可访问性 - 页面只有一个 `h1`,每个分区是带 `aria-labelledby` 的 `section`,标题是 `h2`;控制面板里的分组是带标题的 `role="group"`。 - 宽屏导航是 `nav`,当前分区带 `aria-current="page"`;圆标除了数字还有读屏文字「已修改 N 项」。窄屏顶部的分段选择是单选组,有 `aria-label`,方向键切换。 - 所有控件沿用各自的语义:滑块是原生滑块(方向键、Home / End、PageUp / PageDown),区间滑块的两个手柄分别叫「起点」「终点」,读数同时作为 `aria-valuetext`;开关是 `role="switch"`;色调选择与分段选择是单选组。 - 改过的项不只靠颜色区分:标签变亮之外,面板头部、导航圆标和保存栏都有文字计数。 - 保存状态是 `role="status"`,保存失败是 `role="alert"`,会立即朗读;保存中按钮禁用并显示「保存中…」,避免重复提交。 - 预览卡片是纯展示,对读屏隐藏;图标按钮都有中文名称,焦点环可见。 - 视图切换动画遵循 `prefers-reduced-motion`,开启后只保留淡入;预览里的脉冲点在「减少动态效果」打开时也会静止。 ## 注意事项 ### 文件结构 ```text app/settings/page.tsx # 页面入口:把 onSave 接到你的后端 components/settings-01/ ├── settings-01.tsx # 入口:状态、已修改计数、分区切换、底部保存栏 ├── config.ts # ★ DEFAULT_LABELS 文案、DEFAULT_VALUES 默认值、分区与键的对应 ├── types.ts # Settings01Values 等类型 ├── utils.ts # 值比较、小时格式化、错误文案 ├── section-layout.tsx # 分区外壳:标题、控制面板头部(复制 / 恢复)、右侧预览位 ├── section-nav.tsx # 宽屏左侧导航 + 窄屏顶部分段选择 ├── appearance-section.tsx # 外观 ├── notifications-section.tsx # 通知 ├── sound-section.tsx # 声音与触感 ├── editor-section.tsx # 编辑器 ├── privacy-section.tsx # 隐私 ├── previews.tsx # 外观预览卡片、编辑器代码预览 └── use-save-action.ts # 保存的 loading / 成功 / 错误状态 ``` - **已修改的口径**:「已修改」比的是当前值与默认值(`defaults`),「有未保存的更改」比的是当前值与上次保存(或首次载入)的值;两者独立。恢复默认后,如果和上次保存的值不同,「保存」仍然可点,用来把重置存下来。 - **免打扰时段**:区间滑块的两个手柄不能交叉,而免打扰通常跨午夜,所以滑块范围取 12:00 到次日 12:00(值域 12–36),读数对 24 取模。需要全天范围时改 `notifications-section.tsx` 里的 `QUIET_MIN` / `QUIET_MAX`。 - **预览里的深色**:深色靠在卡片上套用 `.dark` 变量实现,站点本身已是深色时,选「浅色」无法在卡片里反向还原,所以预览标题栏另外写出了当前模式。 - **强调色**:`accent` 同时传给所有控件的 `tone`,并作为 `--flow-tone` 设在最外层,导航选中标记、预览卡片都跟着变;`primary` 跟随当前颜色主题。 - **增删设置项**:在 `types.ts` 加键、`config.ts` 的 `DEFAULT_VALUES` 和 `SECTION_KEYS` 登记,再到对应分区文件里加控件即可,计数、复制和恢复默认会自动包含它。 - **静态导出**:组件是纯客户端组件,不使用 Server Actions 或 `cookies()`,可用于 `output: "export"`。 --- # 角度刻度尺 来源:https://yiui.dev/components/angle-dial/ ## 安装 ## 用法 ```tsx title="components/straighten-dial.tsx" import { useState } from "react" import { AngleDial } from "@/components/ui/angle-dial/angle-dial" export function StraightenDial() { const [angle, setAngle] = useState(0) return ( ) } ``` `value` / `defaultValue` / `onValueChange` 与站内其他方条控件同一套约定:给 `value` 就是受控,不给就由组件自己管,`defaultValue` 只决定起点与复位目标。外框、圆角与高度跟数值滑块完全一致,同一个面板里并排摆不会错位。 ### 拖动刻度,读正中指针 刻度尺里躺着的是一段转盘,固定在正中的竖线是读数指针。按住刻度往左拖,刻度跟着手走,指针就读到更大的角度——每移动 `pixelsPerDegree` 像素走一度,默认是 `3`;调大更细,调小更灵敏。刻度每 5° 一根短线、每 15° 一根长线,两端渐隐,像从窗口里露出的一段转盘。 右侧是格式化后的读数,以及一个跟着转的小表盘,扫一眼就知道现在偏向哪边。 ### 键盘与复位 刻度尺自己可以聚焦,键盘操作与数值滑块一致: | 按键 | 变化 | | --------------------- | ------------------------- | | `←` `↓` | 减一个 `step` | | `→` `↑` | 加一个 `step` | | `Shift` + 方向键 | 按 `largeStep`(默认 15) | | `PageUp` / `PageDown` | 按 `largeStep` | | `Home` / `End` | 到两端 | 双击刻度尺回到 `defaultValue`;不传 `defaultValue` 时回到 0,0 不在范围内时取 `min`。拖动松手、每次键盘变更、双击复位之后都会触发 `onValueCommitted`,需要「松手才算一次」时用它。 ### 首尾相接 `max - min` 正好是 360 时自动首尾相接:范围是一整圈,越过 `max` 会从 `min` 接着走,360° 与 0° 是同一个角度,所以读数显示成 `min`。首尾相接时按 `End` 会停在 `max - step`(即离一圈终点最近的那一档),不会停在和起点重合的位置。 给别的范围也可以手动打开:传 `wrap` 即可,`wrap={false}` 则关掉自动判断。 ```tsx ``` ### 高度、色调与「已改动」 `variant` 与 `size` 的组合高度和数值滑块同一张表: | `variant` | `sm` | `default` | `lg` | | --------- | ---- | --------- | ---- | | `bar` | 28px | 32px | 36px | | `compact` | 18px | 22px | 24px | `tone` 只给指针与小表盘上色,取 `--flow-tone`;不传时用前景色。`modified` 把标签与读数从次要色提到前景色,并在根元素上留一个 `data-modified`,通常写成 `modified={value !== defaultValue}`,让控制面板里改过的项一眼可辨。 ```tsx ``` ## 示例 ### 罗盘方位 范围 0–360 正好一圈,`wrap` 自动打开:拖过 360° 会从 0° 接着走。罗盘指针与刻度尺右侧的小表盘同步转动,读数旁标出八方位名。`largeStep={45}` 让 `Shift` + 方向键一次跳一个方位。 ### 渐变角度 色块的 `linear-gradient` 角度直接来自刻度,下面一行是这段 CSS 的简写。渐变色只用主题提供的 `--flow-*` 变量,亮暗与颜色主题切换时一起变。 ## API ### AngleDial 默认 `min={-180}`、`max={180}`、`step={1}`、`largeStep={15}`、`pixelsPerDegree={3}`、`variant="bar"`、`size="default"`;`wrap` 不传时按「范围是否正好 360°」自动决定,`formatValue` 默认是 `数值°`,`modified`、`disabled` 默认关闭,`tone` 不传时用前景色。除上表外,其余属性直接落在根元素 `
` 上。 ### angleDialVariants `cva` 生成的根元素样式函数,有 `size` 与 `variant` 两个变体。高度通过根元素的 `--angle-dial-height` 变量传给内部,自己写包装层时可以复用它保持同一套高度。 ```ts const angleDialVariants: (props?: { size?: AngleDialSize variant?: AngleDialVariant }) => string ``` ### 类型 ```ts type AngleDialSize = "sm" | "default" | "lg" type AngleDialVariant = "bar" | "compact" type AngleDialProps = Omit< React.ComponentProps<"div">, "defaultValue" | "onChange" | "children" > & { value?: number defaultValue?: number onValueChange?: (value: number) => void onValueCommitted?: (value: number) => void min?: number max?: number step?: number largeStep?: number wrap?: boolean pixelsPerDegree?: number label?: React.ReactNode formatValue?: (value: number) => string variant?: AngleDialVariant size?: AngleDialSize tone?: FlowTone modified?: boolean disabled?: boolean } ``` ## data-slot | 元素 | `data-slot` | | ---------- | ------------------- | | 根元素 | `angle-dial` | | 左侧标签 | `angle-dial-label` | | 刻度尺 | `angle-dial-ruler` | | 刻度 | `angle-dial-ticks` | | 指针 | `angle-dial-needle` | | 读数与表盘 | `angle-dial-value` | | 小表盘 | `angle-dial-glyph` | 根元素始终带 `data-variant`(`bar` / `compact`);拖动中带 `data-dragging`,`modified` 为真时带 `data-modified`,禁用时带 `data-disabled`。 ## 可访问性 - 刻度尺是 `role="slider"`,带 `aria-valuemin` / `aria-valuemax` / `aria-valuenow` 与 `aria-valuetext`。`aria-valuetext` 用的就是 `formatValue` 的结果,所以把读数换成「东北」「+12°」这类写法后,读屏念的也是它。 - 名称优先取 `label`(`aria-labelledby`),没有 `label` 时用 `aria-label`,两者都没有就退回「角度」——同一个表单里有多个刻度尺时务必给名称。 - 刻度、指针、读数与表盘都是 `aria-hidden` 的装饰,读屏只念上面那几个值。 - 焦点环只在键盘聚焦时出现(`focus-visible`)。鼠标按下刻度尺不会留下焦点框,但焦点会落到刻度尺上,可以用方向键接着微调。 - `disabled` 时去掉 Tab 可达性(`tabIndex={-1}`)、指针交互与键盘响应,并降到半透明。 - 系统开启「减少动态效果」时指针不再在拖动中轻微放大。 ## 注意事项 - 组件在服务端就输出完整结构(刻度按当前值平移、表盘指针带着 `rotate()`),不需要预水合脚本,禁用 JS 时也能看到当前值。 - `formatValue`、`defaultValue` 建议定义在组件外,避免每次渲染都换一个新函数或新对象。 - 拖动期间的读数变化会正常触发 React 渲染;一次拖动里读数会连续刷新,这是有意的。 - 用作表单字段时外面自己套 `input type="hidden"`,组件只提交到 `onValueChange` / `onValueCommitted`。 - 受控模式下记得同时处理 `onValueChange`,否则拖动后值会弹回。 - `tone` 的颜色变量由 `flow-diagram` 条目提供,安装时会一并拉取;手动安装需要自己补上 `--flow-*` 变量。 ## 源码 --- # 卡片堆叠 来源:https://yiui.dev/components/card-stack/ ## 安装 ## 用法 `CardStackItem` 直接作为 `CardStack` 子级,内容自由组合。`CardStackControls` 放在同一级,提供上一张、下一张、指示点与暂停按钮。也可以通过 `items` 传入 React 节点数组,子级里只保留控制器。 ```tsx title="components/card-stack-example.tsx" "use client" import { CardStack, CardStackItem, CardStackControls, } from "@/components/ui/card-stack/card-stack" export function CardStackExample() { return (

个人组件库

从重复的界面里提炼原语。

开发者笔记

记录解决问题的过程。

) } ``` ## 示例 每个示例的标题栏右侧都有「调整」开关:打开后可以实时调整模式、轮换间隔、层叠深度、偏移与缩放,并开关暂停;未改动的项显示示例自己的取值。调整时预览画框大小不变。 ### 引言堆叠 大引号、正文与署名组成卡片,内容高度由所有卡片的最大高度撑开,切换时页面不会跳动。示例文案为虚构内容。 ### 拖拽预览 `mode="drag"` 关闭定时轮换。拖动距离超过 80px 时顶卡飞出,并进入循环序列底部;未超过阈值则回弹。触屏保留纵向页面滚动。 ### 指标卡 卡片内部可直接组合 `stat-card` 的标题、数值和趋势原语。堆叠本身不依赖统计卡;该 registry 依赖供此示例组合使用。 ## API ### CardStack | 属性 | 默认值 | 说明 | | ---------------------- | ---------- | ------------------------------------------------ | | `mode` | `auto` | `auto` 定时轮换;`drag` 手动拖动 | | `interval` | `4000` | 轮换间隔,毫秒,最小 1000 | | `depth` | `3` | 可见层数,限制在卡片数量以内 | | `offset` | `12` | 每层向下偏移的像素,最小 0 | | `scale` | `0.04` | 每层缩小的比例;累计缩小最多 20% | | `index / defaultIndex` | `— / 0` | 受控索引或非受控初始索引,按卡片数量循环归一化 | | `onIndexChange` | `—` | 接收新的零起始索引;受控时由调用方更新 `index` | | `items` | `—` | React 节点数组,与直接子级卡片择一使用 | | `label` | `卡片堆叠` | 轮播区域的可访问名称 | | `labels` | 中文 | 覆盖前后切换、暂停、继续、卡片编号和轮播角色文案 | 自动模式在悬停、焦点位于组件内、离开视口或页面隐藏时暂停,恢复后重新计时。暂停按钮可持续暂停轮换。若不放控制器,调用方应提供等价的暂停入口。 ### CardStackItem 细边框、无阴影的卡片容器;支持原生 `div` 属性。仅顶卡可交互,其他卡片使用 `inert` 与 `aria-hidden`。 ### CardStackControls 按钮统一使用 `icon-sm`(28px)。指示点是带 `aria-pressed` 的按钮;自动模式附带暂停控制,零张或单张卡片时前后切换按钮禁用。 ### 调整 Context `CardStackTuningContext` 接收 `CardStackTuning` 覆盖项;`onReport(report: CardStackReport)` 回报覆盖前的实际属性。 `stableFrame` 固定调整期间的画框,不改变图形比例。 ## 可访问性 - 根容器可聚焦;左右方向键切换,控制器按钮也支持原生键盘操作。 - 卡片内的输入框、滑块等保留自己的方向键操作,不拦截内部内容的键盘事件。 - 自动轮换不持续播报;手动操作或暂停时,当前卡片编号通过礼貌播报区更新。 - 减少动态效果时取消层叠位移、缩放和飞出,以淡入切换;拖拽仍可完成切换。 - 自动轮换时建议始终保留 `CardStackControls`,便于用户主动暂停。 ## 注意事项 - 调整面板通过 `CardStackTuningContext` 覆盖子树里的 `mode`、`interval`、`depth`、`offset`、`scale`、`paused`,并用 `onReport` 回报示例自己的取值。不包这一层时组件行为不变;它面向调试与文档,正常使用不需要。 - 演示使用固定数据,首屏不依赖随机数或当前时间,适用于静态导出。 - 卡片与控制器需直接放在根容器下;`items` 支持任意节点。动态列表请使用稳定的 React key。 - 拖拽时会暂时超出卡片边缘,请按场景在外部预留空间或设置裁切区域。 - 指标示例的统计格式与涨跌约定沿用 `stat-card`。 ## 源码 --- # 图表卡片 来源:https://yiui.dev/components/chart-card/ ## 安装 条目依赖 shadcn 的 `chart`(`recharts` 的封装),安装时会一并添加。 ## 用法 `ChartCard` 是外壳,分 `Header` / `Content` / `Footer` 三个区域,区域之间用细线隔开;图表放在 `Content` 里。 ```tsx title="app/traffic.tsx" import { ChartCard, ChartCardContent, ChartCardDescription, ChartCardHeader, ChartCardTitle, ChartSeries, } from "@/components/ui/chart-card/chart-card" const data = [ { day: "2026-02-01", docs: 6200, blog: 4800 }, { day: "2026-02-02", docs: 6900, blog: 5100 }, // … ] export function Traffic() { return (
站点流量 每日页面访问量
) } ``` | 组件 | 用途 | | --- | --- | | `ChartCard` + `Header` / `Title` / `Description` / `Action` / `Content` / `Footer` | 卡片布局 | | `ChartMetrics` + `ChartMetric` | 竖线隔开的指标行 | | `ChartTable` | 图表下方的明细表,首列带与序列同色的竖条 | | `ChartGauge` | 刻度式环形仪表 | | `ChartSeries` | 折线 / 柱 / 面积,支持堆叠与正负对比 | | `ChartRanking` | 横向排名条 + 图例格 | | `ChartLegendList` / `ChartLegendGrid` | 行内图例、图例格 | 页面顶部的预览把几个部件拼在了一张卡片里:左边 `ChartGauge` 显示目标进度,右边 `ChartSeries` 画两条面积序列,头部的 `ChartMetrics` 用同色圆点与序列对应。 ## 示例 ### 刻度式环形仪表 `ChartGauge` 由一圈细刻度组成,按值依次点亮:点亮的刻度由淡到浓,最后一格最亮最长,像指针停在当前值上。`ticks` 控制刻度数,`arc` 控制总角度(默认 270° 下方留口,360 为整圈),`fade={false}` 关掉渐变,`children` 替换中央内容。 ### 多序列折线与明细表 `ChartSeries` 默认画折线;序列多于一条时在上方显示图例。图表下方的 `ChartTable` 用 `columns` 和 `rows` 描述明细,行里的 `tone` 对应图表里的序列。 ### 堆叠柱与指标行 `type="bar"` 加 `stacked` 得到堆叠柱;头部的 `ChartMetrics` 把几个关键指标并排摆在一起。 ### 正负对比 `stacked="diverging"` 让正值向上、负值向下堆叠,适合新增与流失、收入与支出这类方向相反的数据:把流失记为负数,两组数据就落在零线两侧,提示框里仍显示各自的数值。 ### 排名条 `ChartRanking` 用名次作纵轴,条末标出数值,旁边(窄屏在下方)是带名称的图例格。百分比数据传 `max={100}`,其他数据默认取最大值作满值。 ### 配色与坐标轴 序列没有指定 `tone` 时按顺序取默认色板:蓝、琥珀、绿、紫、玫红、青。默认色板里没有主题强调色——强调色会落在其中某一种附近,切到对应的颜色主题时会和那条序列撞色;单条序列想跟随主题时显式写 `tone="primary"`。颜色全部来自主题变量,14 个颜色主题和亮暗模式下都能正确显示。`tone` 也可以是 `chart-1` … `chart-5`、`up` / `down`(涨跌色,随 `convention` 变化)或任意 CSS 颜色。 坐标轴字号 11px,网格只有水平虚线,颜色是线条网格的 `--edge`。日期轴用 `formatDay` 变成「2月14日」或「02/14」,星期轴是单字「一…日」,纵轴整条轴共用一个单位:按数据最大值选「万 / 亿」(`en` 为 K / M / B),所有刻度都换算成它,不会出现 `9000` 与 `1.2万` 混在一起;数据的最大值不到 1 万时刻度都是普通整数。 ## API ### ChartCard ### ChartMetric ### ChartTable ### ChartGauge ### ChartSeries ### ChartRanking ### ChartLegendList ### ChartLegendGrid `ChartCardHeader`、`ChartCardTitle`、`ChartCardDescription`、`ChartCardAction`、`ChartCardContent`、`ChartCardFooter`、`ChartMetrics` 是带样式的容器,属性同对应的原生元素。 ## 可访问性 - 图表容器是 `role="img"`,名称来自 `label`;悬停提示框读取精确数值。需要让读屏用户拿到数据时,配一张 `ChartTable` 或在文字里写出关键数字。 - `ChartGauge` 是 `role="meter"`,带 `aria-valuenow` 和 `aria-valuemax`;中央的数字是真实文本。 - `ChartTable` 用语义化的 ``,表头 `scope="col"`,窄屏横向滚动而不是挤压。 - 序列靠图例的名称区分,不只靠颜色。 - 开启「减少动态效果」后,recharts 的入场动画关闭,仪表的刻度不再依次淡入;仪表的刻度由服务端直接输出,淡入是纯 CSS,没有脚本也完整显示。 ## 注意事项 - 图表由 `ResponsiveContainer` 撑满容器,服务端渲染时先按 320×200 占位,水合后量出真实尺寸;`ChartSeries` 的高度用 `className`(默认 `h-64`)控制。 - 纵轴宽度用 `width="auto"` 按刻度文字自适应,需要和别的图表对齐时给相同的 `yDomain` 与 `yTicks`。 - 涨跌配色约定见 [统计卡片](/components/stat-card#涨跌配色约定);`ChartCard` 与 `ChartSeries` 也接受 `convention`。 ## 源码 --- # 手写清单 来源:https://yiui.dev/components/checklist/ ## 安装 ## 用法 `Checklist` 提供尺寸、色调与自动播放,`ChecklistList` 放条目,`ChecklistItem` 是一条带复选框的任务。勾选时文字变淡,并被一道略有起伏的手写划线划掉。划线宽度量自文字本身,中文、英文、数字都能完整覆盖。 ```tsx title="components/checklist-example.tsx" import { Checklist, ChecklistItem, ChecklistList, ChecklistProgress, } from "@/components/ui/checklist/checklist" export function ChecklistExample() { return ( 写完设计稿 补上暗色截图 发布 ) } ``` `ChecklistItem` 同时支持受控与非受控:传 `checked` 与 `onCheckedChange` 由你管理状态,只传 `defaultChecked` 则组件自己管。`ChecklistItem` 要作为 `ChecklistList` 的直接子级(用 `map` 生成也可以),自动播放按子级顺序编号。页面顶部的预览给 `Checklist` 加了 `autoplay`,进入视口后会自己逐条划掉;每条用 `tone` 指定不同的划线色,用 `meta` 在行尾放耗时之类的辅助信息。 ## 示例 ### 交互式 非受控条目配合进度条,点击任意一条划掉或恢复,计数实时更新。划线与勾选后的复选框填充用 `tone` 换成蓝色;进度条的色调单独用 `ChecklistProgress` 的 `tone` 指定。 ### 紧凑款 `size="sm"` 缩小复选框与字号,适合侧栏和小卡片。划线与复选框换成玫红,像红笔批改。 ## API ### Checklist `autoplay` 让清单按顺序自动划掉,全部完成后停 `resetDelay` 毫秒再清空重来。播放期间条目只读;进入视口才开始,离屏、`paused`、减少动态效果时停在静态快照(划掉约六成)。 ### ChecklistList 渲染 `
    ` 并给每个子级编号,同时把条目数告诉 `Checklist`。接受 `ul` 的全部属性。 ### ChecklistItem ### ChecklistProgress 「已完成 / 总数」加进度条,手动勾选和自动播放都会实时更新。总数默认等条目挂载后统计,传 `total` 则服务端输出时也能直接显示准确计数。 ## 可访问性 - 复选框使用 Base UI 的 Checkbox,整行是 `