示例组件
2026/5/15大约 4 分钟euvuirustwasmexamplecomponent
Button 组件
示例项目包含两种按钮组件:primary_button(全宽按钮,移动端自动拉伸)和 modal_primary_button(模态框内按钮,移动端不全宽)。
#[derive(Default)]
struct PrimaryButtonProps {
label: &'static str,
onclick: Option<Rc<dyn Fn(Event)>>,
disabled: bool,
children: VirtualNode,
}
#[component]
pub(crate) fn primary_button(node: VirtualNode<PrimaryButtonProps>) -> VirtualNode {
let PrimaryButtonProps { label, onclick, disabled, children } = node.try_get_props().unwrap_or_default();
let content: VirtualNode = match children {
VirtualNode::Empty => VirtualNode::Text(TextNode::new(label.to_string(), None)),
other => other,
};
html! {
button {
class: if { disabled } { c_primary_button_disabled() } else { c_primary_button() }
onclick: onclick
content
}
}
}
#[component]
pub(crate) fn modal_primary_button(node: VirtualNode<PrimaryButtonProps>) -> VirtualNode {
// 与 primary_button 相同的 Props,但使用不同的 CSS 类
// 移动端不全宽,适用于模态框场景
}使用:
html! {
primary_button {
label: "Click me"
onclick: move |_event: Event| { /* 处理点击 */ }
"Click me"
}
primary_button {
label: "Disabled"
disabled: true
"Disabled Button"
}
}Badge 组件
#[derive(Default)]
struct MyBadgeProps {
color: &'static str,
text: &'static str,
outline: bool,
on_click: Option<Rc<dyn Fn(Event)>>,
}
#[component]
pub fn my_badge(node: VirtualNode<MyBadgeProps>) -> VirtualNode {
let MyBadgeProps { color, text, outline, on_click } = node.try_get_props().unwrap_or_default();
if outline {
html! {
span {
class: c_badge_outline()
style: { color: {color}; border-color: {color}; }
onclick: on_click
text
}
}
} else {
html! {
span {
class: c_badge()
style: { background: {color}; }
onclick: on_click
text
}
}
}
}使用:
html! {
my_badge {
color: "#059669"
text: "Success"
on_click: move |_event: Event| { /* 点击处理 */ }
}
}Card 组件
#[derive(Default)]
struct MyCardProps {
title: &'static str,
children: VirtualNode,
}
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, children } = node.try_get_props().unwrap_or_default();
html! {
div {
class: c_card()
h3 {
class: c_card_title()
title
}
children
}
}
}使用:
html! {
my_card {
title: "Card Title"
p { "Card content" }
}
}FormInput 组件
#[derive(Default)]
struct FormInputProps {
id: &'static str,
label: &'static str,
placeholder: &'static str,
value: &'static str,
autocomplete: &'static str,
}
#[component]
pub fn form_input(node: VirtualNode<FormInputProps>) -> VirtualNode {
let FormInputProps { id, label: label_string, placeholder, value, autocomplete } = node.try_get_props().unwrap_or_default();
html! {
div {
class: c_form_input_wrapper()
label {
r#for: id
class: c_form_label()
label_string
}
input {
id: id
name: id
r#type: "text"
placeholder: placeholder
value: value
autocomplete: autocomplete
class: c_form_input()
}
}
}
}使用:
html! {
form_input {
label: "Username"
placeholder: "Enter your username"
value: ""
}
}Modal 组件
模态框组件支持弹出/关闭、遮罩层点击关闭和自定义内容:
#[derive(Default)]
struct MyModalProps {
title: &'static str,
onclick: Option<Rc<dyn Fn(Event)>>,
children: VirtualNode,
}
#[component]
pub fn my_modal(node: VirtualNode<MyModalProps>) -> VirtualNode {
let MyModalProps { title, onclick, children } = node.try_get_props().unwrap_or_default();
html! {
div {
class: c_modal_overlay()
onclick: onclick.clone()
div {
class: c_modal_content()
onclick: move |_event: Event| {}
div {
class: c_modal_header()
h3 { class: c_modal_title() title }
button {
onclick: onclick
"×"
}
}
div {
class: c_modal_body()
children
}
}
}
}
}使用:
html! {
my_modal {
title: "Confirm"
onclick: move |_event: Event| { show_modal.set(false); }
p { "Are you sure?" }
}
}VConsole 组件
虚拟控制台组件,在页面底部提供半屏抽屉式调试面板,支持日志级别过滤和清除:
- vconsole_panel — 顶级面板组件,协调浮动按钮和抽屉面板
- vconsole_fab — 浮动操作按钮(带未读日志数徽标),点击展开面板
- vconsole_drawer — 半屏抽屉面板,包含日志条目列表、级别过滤栏和清除/关闭按钮
#[derive(Clone, Default)]
pub(crate) struct VconsolePanelProps {
panel_open: Signal<bool>,
}
#[component]
pub(crate) fn vconsole_panel(node: VirtualNode<VconsolePanelProps>) -> VirtualNode {
let VconsolePanelProps { panel_open } = node.try_get_props().unwrap_or_default();
// 渲染浮动按钮 + 抽屉面板
}使用:
html! {
vconsole_panel {
panel_open: show_console_signal
}
}VConsole 提供日志 API(Console::log、Console::warn、Console::error、Console::clear),需要在应用入口调用 init_console() 初始化:
pub(crate) fn app() -> VirtualNode {
init_console();
// ...
}PageHeader 组件
页面头部组件,为每个演示页面提供统一的标题和副标题结构。与 #[component] 组件不同,page_header 是普通函数组件,直接接收参数而非 Props 结构体:
pub(crate) fn page_header(title: &str, subtitle: &str) -> VirtualNode {
html! {
div {
class: c_page_header()
h1 {
class: c_page_title()
title
}
p {
class: c_page_subtitle()
subtitle
}
}
}
}使用:
html! {
page_header("Signals", "Explore reactive signals")
}LogoButton 组件
品牌 Logo 按钮组件,渲染带渐变背景的 "E" 字母标识。根据是否提供点击处理器,渲染为 <button> 或 <span> 元素。用于导航侧边栏 Logo 和调试控制台浮动按钮。
#[derive(Clone, Default)]
pub(crate) struct LogoButtonProps {
variant: LogoButtonVariant,
on_click: Option<Rc<dyn Fn(Event)>>,
}
/// 显示变体
pub(crate) enum LogoButtonVariant {
Nav, // 导航侧边栏内联 32×32 按钮
Fab, // 固定位置 48×48 浮动操作按钮
}
#[component]
pub(crate) fn logo_button(node: VirtualNode<LogoButtonProps>) -> VirtualNode {
let LogoButtonProps { variant, on_click } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
// 根据 variant 和 on_click 渲染不同样式和元素类型
}使用:
html! {
// 可点击的导航 Logo
logo_button {
variant: LogoButtonVariant::Nav
on_click: move |_event: Event| { /* 导航处理 */ }
}
// 不可点击的标识(如锚点内)
logo_button {
variant: LogoButtonVariant::Fab
}
}VirtualList 组件
虚拟列表组件,实现高性能的窗口化列表渲染。仅渲染可见区域内的 DOM 节点加上少量 overscan 缓冲项,无论列表总大小如何,DOM 节点数量保持恒定。
/// 虚拟列表状态
#[derive(Clone, Copy, Data, New)]
pub(crate) struct UseVirtualList {
scroll_offset: Signal<i32>,
viewport_height: Signal<i32>,
}
/// 创建虚拟列表状态
pub(crate) fn use_virtual_list() -> UseVirtualList
/// 计算可见项范围
pub(crate) fn compute_visible_range(
scroll_offset: i32,
viewport_height: i32,
total_count: usize,
item_height: i32,
overscan_count: usize,
) -> (usize, usize)
/// 创建滚动事件处理器
pub(crate) fn virtual_list_on_scroll(state: UseVirtualList) -> Option<Rc<dyn Fn(Event)>>使用:
#[component]
pub(crate) fn page_virtual_list(node: VirtualNode<PageVirtualListProps>) -> VirtualNode {
let state: UseVirtualList = use_virtual_list();
let (start_index, end_index): (usize, usize) = compute_visible_range(
state.get_scroll_offset().get(),
state.get_viewport_height().get(),
total_count,
item_height,
overscan_count,
);
html! {
div {
onscroll: virtual_list_on_scroll(state)
div {
style: format!("position: relative; height: {total_height}px;")
div {
style: format!("position: absolute; top: {top_padding}px;")
for index in { start_index..end_index } {
div {
key: index.to_string()
{ format!("Item #{index}") }
}
}
}
}
}
}
}提示
组件嵌套直接在标签内嵌套即可:primary_button { my_badge { text: "Nested" } }