当前位置: 首页 > news >正文

Rust 全栈项目里,我写了一个不再重复造轮子的泛型表格组件

最近在用 Rust + Leptos 写一个家政行业的 CRM 系统,后台管理页面里表格是绝对的主角——客户列表、订单列表、排班列表、服务项目列表……每个页面都要一个表。

刚开始我也是老老实实每个页面手写<table>,写了三个页面后实在受不了了,于是抽了一个泛型表格组件出来。这篇文章就来聊聊DaisyTable的设计思路和其中 3 个关键模式。

问题:为什么手写表格撑不住

传统的做法很简单:每个页面自己写<For>循环渲染行,每行自己写<td>。看起来没问题,但三个页面后你会发现:

  • 排序、加载态、空状态,每个页面都要重写一遍
  • 列定义散落在视图代码里,维护起来得扒拉半天
  • 操作列里的"查看/编辑/删除"按钮,每行拿当前行数据的方式各不相同

目标很明确:定义一个泛型表格,用一个数据结构描述"有哪些列",每个列自带渲染逻辑,行数据通过 Provider 机制自动注入,排序、加载态、空态全部内置。

设计一:Column 插槽 —— 用声明式 DSL 描述列

Leptos 的#[slot]宏让我们可以定义"插槽组件"——父组件接收一组子组件作为配置项。Column 就是这样一个插槽:

#[derive(Clone)]#[slot]pubstructColumn{publabel:String,#[prop(default = false)]freeze:bool,#[prop(default = false)]sort:bool,prop:String,#[prop(optional, into)]pubclass:Option<String>,pubchildren:ChildrenFn,}

使用时就是一个声明式的 DSL,列定义和渲染逻辑写在一起:

<DaisyTabledata=data on_sort=on_sort><Columnslot:columns freeze=trueprop="user_name".to_string()label="姓名".to_string()class="font-bold"sort=true>{letuser:Option<Contact>=use_context::<Contact>();view!{<span class="font-medium">{user.map(|u|u.user_name).unwrap_or_default()}</span>}}</Column><Columnslot:columns label="电话".to_string()prop="phone_number".to_string()>{letuser:Option<Contact>=use_context::<Contact>();view!{<span>{user.map(|u|u.phone_number).unwrap_or_default()}</span>}}</Column>// ... 更多列</DaisyTable>

每个 Column 的children是一个闭包,由表格内部在渲染时调用,你只管定义"这一列长什么样"。freeze列渲染为<th>(表头单元格样式),普通列渲染为<td>

设计二:Provider 注入 —— 子组件无感获取当前行数据

这是整个设计里最巧妙的地方。看表格内部的渲染逻辑:

<Foreach=move||items key=|item|item.id()children=move|item|{view!{<Providervalue=item><tr><Foreach=move||columns key=...children=move|(_,col)|{ifcol.freeze{view!{<th class=...>{(col.children)()}</th>}}else{view!{<td class=...>{(col.children)()}</td>}}}/></tr></Provider>}}/>

注意<Provider value=item>把当前行数据注入了上下文。所以每个 Column 的 children 闭包里,可以直接use_context::<T>()拿到当前行数据,不需要手动传参、不需要闭包捕获、不需要索引访问

举个例子:操作列里需要当前行的contact_uuid来做编辑和删除,传统做法要通过闭包层层传参,而这里直接:

<Columnslot:columns freeze=truelabel="操作".to_string()prop="".to_string()>{letuser:Option<Contact>=use_context::<Contact>();letuuid=user.map(|u|u.contact_uuid).unwrap_or_default();view!{<button on:click=move|_|delete(uuid.clone())class="btn btn-ghost btn-xs">"删除"</button>}}</Column>

Provider 充当了"当前行作用域"的角色。每一列的子组件不用知道自己在第几行、数据怎么传进来的——直接从上下文拿即可。

设计三:Identifiable —— 让 Leptos 精确知道谁变了

Leptos 的<For>组件需要一个稳定的key来判断列表项的新增、删除和移动。所以我们定义了一个 trait:

pubtraitIdentifiable{fnid(&self)->String;}

业务数据类型只需实现它:

implIdentifiableforContact{fnid(&self)->String{format!("{}-{}",self.contact_uuid,self.updated_at)}}

为什么要拼updated_at如果只用contact_uuid,编辑某个客户后数据刷新了,但 key 没变,Leptos 不会重新渲染对应行。把updated_at拼进去后,每次编辑产生新数据时 key 就会变,对应行自然重新渲染。

DaisyTable的泛型约束直接限定T: Identifiable,编译器强制你在使用前思考 key 怎么定义,反过来也避免了忘记给For设置key的常见 bug。

再看看排序和加载态

排序内置在表头里。Column设置sort=true后,表头自动渲染一个ColumnSorter组件:

// ColumnSorter 内部:点击切换 Asc <-> Desc,回调通知父页面lethandle_click=move|_|{letnew_value=sort_value.read().reverse();set_sort_value.set(new_value);ifletSome(f)=on_change.as_ref(){f(new_value);// 通知页面重新请求数据}};

加载态和空状态也内置了。Transition组件包裹<tbody>——数据没准备好时自动渲染 loading 动画;数据为空时自动展示"暂无数据"。这些原本每个页面要手写的逻辑,全部收到表格内部了。

总结

三个核心模式:

  1. Column 插槽:声明式列定义,列结构和渲染逻辑在一起,一眼看清这个表有哪些列
  2. Provider 注入<Provider value=item>+use_context,列子组件无感获取当前行数据,零参数传递
  3. Identifiable trait:编译器强制定义稳定 key,避免渲染漏更新

整个DaisyTable组件(含 ColumnSorter)不到 200 行代码,但已经支撑了 Pico-CRM 里 6 个页面的全部表格渲染——客户管理、订单列表、排班管理、服务项目、商户管理、员工管理。新增一个列表页面只需要实现Identifiable,然后写几个<Column>标签,排序、加载态、空态全部自动到位。

如果你也在用 Rust 写全栈(Leptos / Dioxus / Yew),这种 Provider + Slot 的组合在列表、表单、详情页等场景都可以复用。项目开源在 GitHub。

大家在自己的项目中是怎么处理表格组件的?评论区聊聊。

http://www.jsqmd.com/news/873497/

相关文章:

  • 【GMSK的最大似然序列检测GMSK MLSD】采用维特比算法来解决MLSD问题研究附Matlab代码
  • 微信小程序逆向工程深度解析:wxappUnpacker实用指南
  • 德系多联机在中国市场的技术本土化:从88HP并联到冷凝水回收的十年路径 - 奔跑123
  • 为什么92%的零售AI Agent项目卡在POC阶段?拆解沃尔玛、盒马、屈臣氏内部淘汰的4类伪智能体
  • 2026年4月热门的景点推荐,夜游景点/旅游景点/景点/景区/游玩景点,景点盘点 - 品牌推荐师
  • Cursor Free VIP:告别试用限制,解锁AI编程助手永久Pro权限的技术方案
  • 大模型落地应用全景解析:出海企业如何抓住价值变现新风口?
  • 2026数字营销专业学数据分析的职业优势
  • Boss-Key:职场隐私保护终极指南,一键隐藏窗口的智能解决方案
  • VisoinMaster之单点抓取
  • 2026年,专业人士力荐!聊城那些不容错过的台球器材店机构 - 资讯纵览
  • 靠谱的苏州集成房屋工程工厂哪家质量好 - GrowthUME
  • 2026专业GEO优化服务商TOP推荐(11大全覆盖) - GrowthUME
  • UHF-RFID运动检测技术原理与优化实践
  • Keil中sprintf和自定义Serial_Printf,哪个更适合你的串口打印需求?
  • 个人计算、服务器、工业控制:H5AN8G6NDJR-XNC的DDR4内存颗粒应用版图
  • 十堰第四代住宅装修指南:如何挑选值得信赖的本土装修公司 - GrowthUME
  • 85%企业将淘汰纯业务程序员!2026年前,大模型才是你的职业救命稻草!
  • 飞书秒变 Claude Code 控制台:一个 Bridge 项目,正在改写 AI 编程入口
  • Igalia开发者Yeunjoo Choi谈Chromium:企业浏览器、开源贡献与AI应用
  • 验证旋转中心流程
  • 终极ComfyUI管理器完全指南:轻松管理自定义节点的3种方法
  • 2026最新大模型入门电子书学习推荐,必读9本大模型书籍
  • 合肥租厂房该找谁 - GrowthUME
  • 2026年5月份国内专业GEO搜索推广供应商行业研究报告:洞察市场现状与趋势 - GrowthUME
  • 避开这3个坑,你的IPC图像清晰度调试能省一半时间:Gamma、NR与Dehaze的协同实战
  • 从蓝牙信标到Web地图:用JavaScript在浏览器里玩转RSSI三点定位
  • 全新C#上位机框架SuperSCADA正式发布
  • 广州GEO搜索优化机构实测评测:四大服务商能力对比 - 奔跑123
  • 单相机带角度定位引导