PATTERNS
界面组合
用已有组件组织设置、账户面板与 AI 工作台,查看真实源码和交互边界。
这些组合由真实组件实现,演示状态保存在当前页面。设置与账户示例不连接业务后端,AI 工作台使用静态消息 fixture。
个人设置表单
正在准备交互组合…
组合使用的组件
本地表单演示;onSave 由业务接入,未连接持久化服务。
查看组合源码
"use client";
import { useState } from "react";
import { Button } from "@cwa-design/react";
import { Card, CardActions, CardContent, CardDescription, CardTitle } from "@cwa-design/react";
import { Dialog } from "@cwa-design/react";
import { Field } from "@cwa-design/react";
import { Input } from "@cwa-design/react";
import { Stack } from "@cwa-design/react";
import { Switch } from "@cwa-design/react";
import { createToastManager, ToastProvider } from "@cwa-design/react";
import { RecipeScope as CwaProvider } from "./recipe-scope";
/** Settings recipe 的可见状态(业务侧持有;此处为组件内演示状态)。 */
export interface SettingsValues {
displayName: string;
workspace: string;
notifications: boolean;
}
export interface SettingsRecipeProps {
initialValues?: Partial<SettingsValues>;
onSave?: (values: SettingsValues) => void;
}
/**
* Settings recipe(T31):Field/Input/Select 语义用 Input+原生 select 替代前,
* 用 Field+Input+Switch+Button+Dialog+Toast 的真实组合。
* 原生 <form> 提交;Toast 由 createToastManager 驱动,不连任何模型 API。
*/
export function SettingsRecipe({ initialValues, onSave }: SettingsRecipeProps) {
const [manager] = useState(() => createToastManager());
const [values, setValues] = useState<SettingsValues>({
displayName: initialValues?.displayName ?? "CWA Developer",
workspace: initialValues?.workspace ?? "默认工作区",
notifications: initialValues?.notifications ?? true,
});
const [confirmOpen, setConfirmOpen] = useState(false);
return (
<CwaProvider>
<ToastProvider toastManager={manager}>
<form
onSubmit={(event) => {
event.preventDefault();
onSave?.(values);
manager.add({
title: "已保存设置",
description: onSave ? "已提交给应用" : "已保存在当前演示中",
});
}}
style={{ maxWidth: "32rem" }}
>
<Card>
<CardTitle>个人设置</CardTitle>
<CardDescription>编辑显示名称与通知偏好;保存操作可由应用接入。</CardDescription>
<CardContent>
<Stack gap={4}>
<Field label="显示名称" description="用于工作台中的个人资料">
<Input
name="displayName"
value={values.displayName}
onChange={(e) => setValues((v) => ({ ...v, displayName: e.target.value }))}
/>
</Field>
<Field label="工作区名称">
<Input
name="workspace"
value={values.workspace}
onChange={(e) => setValues((v) => ({ ...v, workspace: e.target.value }))}
/>
</Field>
<Switch
name="notifications"
checked={values.notifications}
onCheckedChange={(checked) =>
setValues((v) => ({ ...v, notifications: checked === true }))
}
>
接收产品通知
</Switch>
</Stack>
</CardContent>
<CardActions>
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<Dialog.Trigger render={<Button variant="secondary">重置…</Button>} />
<Dialog.Content material="solid">
<Dialog.Title>重置为默认值?</Dialog.Title>
<Dialog.Description>
未保存的修改将丢失。此操作可在保存前撤销。
</Dialog.Description>
<Dialog.Close
render={
<Button
variant="secondary"
onClick={() =>
setValues({
displayName: "CWA Developer",
workspace: "默认工作区",
notifications: true,
})
}
>
确认重置
</Button>
}
/>
</Dialog.Content>
</Dialog>
<Button variant="primary" type="submit">
保存设置
</Button>
</CardActions>
</Card>
</form>
</ToastProvider>
</CwaProvider>
);
}
import type { ReactNode } from "react";
import { CwaProvider, useOptionalCwaContext } from "@cwa-design/react";
export function RecipeScope({ children }: { children: ReactNode }) {
const parent = useOptionalCwaContext();
return parent ? children : <CwaProvider>{children}</CwaProvider>;
}
账户面板
正在准备交互组合…
组合使用的组件
活动与账单为静态 fixture;业务操作可通过回调接入,默认只给本地反馈。
查看组合源码
"use client";
import { useState } from "react";
import { Avatar } from "@cwa-design/react";
import { Badge } from "@cwa-design/react";
import { Button } from "@cwa-design/react";
import { Card, CardActions, CardContent, CardDescription, CardTitle } from "@cwa-design/react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
} from "@cwa-design/react";
import { Skeleton } from "@cwa-design/react";
import { Stack } from "@cwa-design/react";
import { Tabs, TabsList, TabsPanel, TabsTab } from "@cwa-design/react";
import { Text } from "@cwa-design/react";
import { RecipeScope as CwaProvider } from "./recipe-scope";
export interface AccountPanelRecipeProps {
/** 静态 fixture;真实业务接入时由调用方传入。 */
name?: string;
plan?: "Pro" | "Team" | "Free";
loading?: boolean;
onSignOut?: () => void;
onAccountSettings?: () => void;
onWorkspaceChange?: () => void;
onManageSubscription?: () => void;
}
const mockActivity: Array<{ label: string; time: string }> = [
{ label: "更新了 API 密钥", time: "2 小时前" },
{ label: "邀请新成员", time: "昨天" },
{ label: "升级到 Pro 计划", time: "上周" },
];
/** AccountPanel recipe(T31):Avatar/Badge/Card/Tabs/DropdownMenu/Skeleton/Alert 组合,静态 fixture。 */
export function AccountPanelRecipe({
name = "李雷",
plan = "Pro",
loading = false,
onSignOut,
onAccountSettings,
onWorkspaceChange,
onManageSubscription,
}: AccountPanelRecipeProps) {
const [feedback, setFeedback] = useState("");
const requestAction = (label: string, callback?: () => void) => {
callback?.();
setFeedback(callback ? `已请求${label}。` : `${label}演示:由宿主应用接入此操作。`);
};
return (
<CwaProvider>
<div style={{ width: "24rem", maxWidth: "100%", minWidth: 0 }}>
<Card>
<CardTitle>
<Stack direction="row" gap={3} style={{ alignItems: "center" }}>
{loading ? (
<Skeleton width={40} height={40} radius={9999} />
) : (
<Avatar fallback={name.slice(0, 1)} />
)}
<Stack gap={1}>
<Text>{name}</Text>
<Badge tone="accent">{plan}</Badge>
</Stack>
<span style={{ marginInlineStart: "auto" }}>
<DropdownMenu>
<DropdownMenu.Trigger
render={
<Button variant="ghost" aria-label="账户操作">
⋯
</Button>
}
/>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => requestAction("账户设置", onAccountSettings)}>
账户设置
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => requestAction("切换工作区", onWorkspaceChange)}
>
切换工作区
</DropdownMenuItem>
<DropdownMenuItem
destructive
onClick={() => requestAction("退出登录", onSignOut)}
>
退出登录
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</span>
</Stack>
</CardTitle>
<Tabs defaultValue="activity">
<TabsList aria-label="账户信息">
<TabsTab value="activity">最近活动</TabsTab>
<TabsTab value="billing">账单</TabsTab>
</TabsList>
<TabsPanel value="activity">
<Stack gap={3}>
{loading
? mockActivity.map((item) => (
<Skeleton key={item.label} width="100%" height="1.25rem" />
))
: mockActivity.map((item) => (
<Stack
key={item.label}
direction="row"
gap={3}
style={{ justifyContent: "space-between", flexWrap: "wrap" }}
>
<Text>{item.label}</Text>
<Text variant="caption" tone="muted">
{item.time}
</Text>
</Stack>
))}
</Stack>
</TabsPanel>
<TabsPanel value="billing">
{loading ? (
<Skeleton width="100%" height="4rem" />
) : (
<Text variant="caption" tone="muted">
下一期账单:2026-11-01 · ¥128/月
</Text>
)}
</TabsPanel>
</Tabs>
<CardActions>
<Button
variant="secondary"
onClick={() => requestAction("管理订阅", onManageSubscription)}
>
管理订阅
</Button>
</CardActions>
<CardContent>
<Text variant="caption" tone="muted" role="status">
{feedback}
</Text>
</CardContent>
</Card>
</div>
</CwaProvider>
);
}
export function AccountSkeletonRecipe() {
return (
<Card>
<CardTitle>加载中…</CardTitle>
<CardContent>
<Stack gap={3}>
<Skeleton width="60%" height="1.25rem" />
<Skeleton width="100%" height="1rem" />
<Skeleton width="80%" height="1rem" />
</Stack>
</CardContent>
<CardDescription>减少动态偏好下闪烁自动关闭,占位几何保持不变。</CardDescription>
</Card>
);
}
import type { ReactNode } from "react";
import { CwaProvider, useOptionalCwaContext } from "@cwa-design/react";
export function RecipeScope({ children }: { children: ReactNode }) {
const parent = useOptionalCwaContext();
return parent ? children : <CwaProvider>{children}</CwaProvider>;
}
AI 工作台界面组合
正在准备交互组合…
组合使用的组件
消息与工具记录为静态 fixture;未连接模型服务。
发送与附件选择提供本地演示,可通过回调接入业务;没有文件上传服务。
查看组合源码
"use client";
import { useRef, useState } from "react";
import { Badge } from "@cwa-design/react";
import { Button } from "@cwa-design/react";
import { Card, CardContent } from "@cwa-design/react";
import { Field } from "@cwa-design/react";
import { Popover, PopoverContent } from "@cwa-design/react";
import { Sheet } from "@cwa-design/react";
import { Stack } from "@cwa-design/react";
import { Tabs, TabsList, TabsPanel, TabsTab } from "@cwa-design/react";
import { Text } from "@cwa-design/react";
import { Textarea } from "@cwa-design/react";
import { RecipeScope as CwaProvider } from "./recipe-scope";
export interface AiWorkspaceRecipeProps {
/** 静态消息 fixture;不连任何模型 API,无模型 key。 */
messages?: Array<{
id: string;
role: "user" | "assistant" | "tool";
text: string;
status?: "ok" | "running" | "error";
}>;
/** 提交给宿主应用;缺省只将消息保存在当前演示页面。 */
onSend?: (text: string) => void;
/** 本地文件选择回调;组件本身不上传文件。 */
onAttach?: (files: File[]) => void;
}
const defaultMessages: NonNullable<AiWorkspaceRecipeProps["messages"]> = [
{ id: "m1", role: "user", text: "帮我总结这份规格文档的验收标准。" },
{
id: "m2",
role: "tool",
text: "retrieve(spec.pdf) → 12 段命中",
status: "ok",
},
{
id: "m3",
role: "assistant",
text: "规格文档共列出 5 条验收标准:安装链路、焦点管理、中断动效、材质对比度、tarball 消费。",
},
];
function StatusBadge({ status }: { status?: "ok" | "running" | "error" }) {
if (!status) return null;
const tone = status === "ok" ? "success" : status === "running" ? "warning" : "danger";
const label = status === "ok" ? "已完成" : status === "running" ? "进行中" : "失败";
return <Badge tone={tone}>{`工具 ${label}`}</Badge>;
}
/** AIWorkspace recipe(T31):Sheet/Popover/Tabs/Text/Button 组合;消息为静态 fixture,无模型 key。 */
export function AiWorkspaceRecipe({
messages = defaultMessages,
onSend,
onAttach,
}: AiWorkspaceRecipeProps) {
const [draft, setDraft] = useState("");
const [localMessages, setLocalMessages] = useState<
NonNullable<AiWorkspaceRecipeProps["messages"]>
>([]);
const [attachments, setAttachments] = useState<File[]>([]);
const [feedback, setFeedback] = useState("");
const fileInput = useRef<HTMLInputElement>(null);
const messageSequence = useRef(0);
return (
<CwaProvider>
<div style={{ width: "30rem", maxWidth: "100%", minWidth: 0 }}>
<Card>
<CardContent>
<Stack gap={4}>
<Stack
direction="row"
gap={3}
style={{ alignItems: "center", justifyContent: "space-between", flexWrap: "wrap" }}
>
<Text style={{ fontWeight: 590 }}>AI 工作台</Text>
<Stack direction="row" gap={2}>
<Popover>
<Popover.Trigger
render={
<Button variant="ghost" aria-label="会话信息">
ⓘ
</Button>
}
/>
<PopoverContent aria-label="会话信息">
<Text variant="caption" tone="muted">
初始消息是演示数据。输入与附件保存在当前页面;模型连接由宿主应用提供。
</Text>
</PopoverContent>
</Popover>
<Sheet>
<Sheet.Trigger render={<Button variant="secondary">会话详情</Button>} />
<Sheet.Content>
<Sheet.Title className="cwa-design-sheet__title">会话详情</Sheet.Title>
<Sheet.Description>工具调用与消息元数据(fixture 数据)。</Sheet.Description>
<Tabs defaultValue="meta">
<TabsList aria-label="会话详情分区">
<TabsTab value="meta">元数据</TabsTab>
<TabsTab value="tools">工具</TabsTab>
</TabsList>
<TabsPanel value="meta">
<Text variant="caption" tone="muted">
会话 ID:fixture-0001 · 模型:无(静态演示)
</Text>
</TabsPanel>
<TabsPanel value="tools">
<Text variant="caption" tone="muted">
retrieve(spec.pdf):已完成 · 12 段命中
</Text>
</TabsPanel>
</Tabs>
<Sheet.Close render={<Button variant="secondary">关闭</Button>} />
</Sheet.Content>
</Sheet>
</Stack>
</Stack>
{[...messages, ...localMessages].map((message) => (
<Stack
key={message.id}
gap={1}
style={{
alignSelf: message.role === "user" ? "flex-end" : "flex-start",
maxWidth: "85%",
}}
>
<Text variant="caption" tone="muted">
{message.role === "user" ? "你" : message.role === "tool" ? "工具" : "助手"}
</Text>
<div
style={{
padding: "var(--cwa-design-space-3)",
borderRadius: "var(--cwa-design-radius-control)",
background:
message.role === "user"
? "color-mix(in srgb, var(--cwa-design-color-accent) 12%, transparent)"
: "var(--cwa-design-color-surface)",
border: "1px solid var(--cwa-design-color-border-subtle)",
overflowWrap: "anywhere",
}}
>
<Text>{message.text}</Text>
</div>
{message.status ? <StatusBadge status={message.status} /> : null}
</Stack>
))}
<form
onSubmit={(event) => {
event.preventDefault();
const text = draft.trim();
if (!text) return;
onSend?.(text);
messageSequence.current += 1;
setLocalMessages((previous) => [
...previous,
{ id: `local-${messageSequence.current}`, role: "user", text },
]);
setDraft("");
setFeedback(
onSend ? "消息已提交给应用。" : "消息已保存在本地演示中,未调用模型。",
);
}}
>
<Stack gap={3}>
<Field label="消息" description="发送后追加到当前会话;演示不会生成模型回复。">
<Textarea
value={draft}
onChange={(event) => setDraft(event.target.value)}
rows={3}
placeholder="输入消息…"
/>
</Field>
<input
ref={fileInput}
type="file"
multiple
className="cwa-design-visually-hidden"
tabIndex={-1}
aria-label="选择本地附件"
onChange={(event) => {
const files = Array.from(event.target.files ?? []);
setAttachments(files);
onAttach?.(files);
setFeedback(
files.length ? `已选择 ${files.length} 个本地附件。` : "已清除附件。",
);
}}
/>
{attachments.length ? (
<Text variant="caption" tone="muted" style={{ overflowWrap: "anywhere" }}>
本地附件:{attachments.map((file) => file.name).join("、")}
</Text>
) : null}
<Stack
direction="row"
gap={2}
style={{ justifyContent: "flex-end", flexWrap: "wrap" }}
>
<Button variant="ghost" onClick={() => fileInput.current?.click()}>
附件
</Button>
{attachments.length ? (
<Button
variant="ghost"
onClick={() => {
setAttachments([]);
if (fileInput.current) fileInput.current.value = "";
onAttach?.([]);
setFeedback("已清除附件。");
}}
>
清除附件
</Button>
) : null}
<Button variant="primary" type="submit" disabled={!draft.trim()}>
发送
</Button>
</Stack>
<Text variant="caption" tone="muted" role="status">
{feedback}
</Text>
</Stack>
</form>
</Stack>
</CardContent>
</Card>
</div>
</CwaProvider>
);
}
import type { ReactNode } from "react";
import { CwaProvider, useOptionalCwaContext } from "@cwa-design/react";
export function RecipeScope({ children }: { children: ReactNode }) {
const parent = useOptionalCwaContext();
return parent ? children : <CwaProvider>{children}</CwaProvider>;
}
媒体工具栏可读配方
正在准备交互组合…
组合使用的组件
媒体背景为样式化占位(无真实照片依赖);接入时用应用实际媒体验证对比度。
Clear 仅在媒体与主题方向一致且文字/必要图标有实色底面时使用;混合亮度请用 Thick/Solid。
查看组合源码
"use client";
import { useState } from "react";
import { Button } from "@cwa-design/react";
import { IconButton } from "@cwa-design/react";
import { Surface } from "@cwa-design/react";
import { Text } from "@cwa-design/react";
import { RecipeScope as CwaProvider } from "./recipe-scope";
/**
* F08:媒体工具栏可读配方。按媒体亮/暗/混合选择前景保护策略:
* - Regular(默认):正文/控件直接可读,适合大多数媒体;
* - Clear(显式启用):仅当媒体与主题方向一致且文字/必要图标有实色底面时使用;
* - Thick/Solid:复杂混合亮度或要求稳定阅读底面时的回退。
* 前景保护通过局部实色底面(图标配色 + 局部 Surface)实现,不要求切换站点主题;
* 文字/图标保持不透明;blur 固定;动效只用 transform/opacity。
*/
export type MediaToolbarTone = "regular" | "clear" | "thick" | "solid";
export interface MediaToolbarRecipeProps {
/** 初始材质;默认 regular(可读性最稳)。 */
initialMaterial?: MediaToolbarTone;
/** 媒体亮度:bright/dark/mixed;用于文案与默认策略说明。 */
media?: "bright" | "dark" | "mixed";
}
const MATERIAL_BY_TONE = {
regular: "glass",
clear: "glass-clear",
thick: "frosted",
solid: "solid",
} as const;
/** 媒体工具栏可读配方(F08)。 */
export function MediaToolbarRecipe({
initialMaterial = "regular",
media = "mixed",
}: MediaToolbarRecipeProps) {
const [material, setMaterial] = useState<MediaToolbarTone>(initialMaterial);
const surfaceMaterial = MATERIAL_BY_TONE[material];
return (
<CwaProvider>
<div
style={{
position: "relative",
minHeight: 280,
borderRadius: 24,
overflow: "hidden",
display: "grid",
alignContent: "center",
justifyItems: "center",
padding: 24,
background:
media === "bright"
? "linear-gradient(135deg, #bfe0f2, #f3ead8)"
: media === "dark"
? "linear-gradient(135deg, #101b2c, #1d2c46)"
: "linear-gradient(110deg, #172434 50%, #ebeddf 50%)",
}}
>
<div aria-hidden="true" style={{ position: "absolute", inset: 0 }}>
<div
style={{
position: "absolute",
top: "18%",
left: "6%",
width: 200,
height: 120,
borderRadius: 999,
background: media === "dark" ? "#0c1526" : "#ffffff",
opacity: 0.6,
}}
/>
<div
style={{
position: "absolute",
bottom: "12%",
right: "8%",
width: 260,
height: 140,
borderRadius: 24,
background: media === "dark" ? "#22354f" : "#9fc3d8",
opacity: 0.55,
}}
/>
</div>
<Surface
material={surfaceMaterial}
style={{
position: "relative",
borderRadius: 20,
padding: "10px 14px",
display: "flex",
gap: 12,
alignItems: "center",
maxWidth: "100%",
}}
>
<span
aria-hidden="true"
style={{
width: 39,
height: 39,
flexShrink: 0,
display: "grid",
placeItems: "center",
borderRadius: 9,
fontSize: 18,
// F08:图标使用实色底面(局部保护),非文本对比按相邻底色测量。
color: "#f6ead6",
background: "linear-gradient(135deg, #346b7f, #1f3f40)",
}}
>
▶
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<strong style={{ fontSize: 14 }}>
{media === "dark" ? "夜色山谷" : media === "bright" ? "高山晨光" : "明暗之间"}
</strong>
<Text variant="caption" tone="muted" as="p" style={{ margin: 0 }}>
{material === "clear"
? "Clear:媒体方向一致 + 图标实色底面"
: material === "thick"
? "Thick:混合亮度的稳妥回退"
: material === "solid"
? "Solid:最长阅读时间的底面"
: "Regular:默认可读"}
</Text>
</div>
{/* 必要图标:♡ 有实色底面(局部 Surface 保护),非文本对比 ≥3:1。 */}
<Surface
material="frosted"
style={{ borderRadius: 999, display: "grid", placeItems: "center" }}
>
<IconButton label="收藏" variant="ghost" size="sm">
♡
</IconButton>
</Surface>
<Button
variant="secondary"
size="sm"
onClick={() =>
setMaterial((m) =>
m === "regular"
? "clear"
: m === "clear"
? "thick"
: m === "thick"
? "solid"
: "regular",
)
}
>
切换材质
</Button>
</Surface>
<Text
variant="caption"
tone="muted"
as="p"
style={{
position: "relative",
margin: "14px 0 0",
textAlign: "center",
background: "rgba(255,255,255,0.72)",
borderRadius: 8,
padding: "2px 10px",
}}
>
F08 配方:媒体亮暗与前景角色决定材质与保护,不需要切换站点主题。
</Text>
</div>
</CwaProvider>
);
}
import type { ReactNode } from "react";
import { CwaProvider, useOptionalCwaContext } from "@cwa-design/react";
export function RecipeScope({ children }: { children: ReactNode }) {
const parent = useOptionalCwaContext();
return parent ? children : <CwaProvider>{children}</CwaProvider>;
}