“Sheet”是一种从屏幕边缘滑入的界面元素,而不是像模态窗口那样在屏幕中央弹出。当你打开电商网站的购物车页面,或者点击筛选图标时,选项内容通常会从侧面滑入屏幕,这种设计你应该已经体验过。
要构建一个能够正确显示长内容的“Sheet”,同时确保页眉和页脚的位置固定不变,并且能够在没有错误的情况下处理表单或计数器的状态信息,仅仅将一个
在本教程中,你将通过Shadcn Space以及Base UI的基本组件,构建两个可用于实际开发的“Sheet”组件:
-
一个购物车Sheet,其中包含数量控制功能、删除商品的功能,以及实时显示的小计金额。
-
一个筛选面板Sheet,配备类别复选框、价格范围滑块、星级评分系统,以及显示当前激活的筛选条件的数量。
完成本教程后,你将能够在自己的项目中使用这两个组件,并且充分理解其背后的状态管理机制与布局设计原理,从而能够自行开发出新的“Sheet”版本。
目录
先决条件
在开始之前,请确保你已经满足以下要求:
-
已安装Node.js 18或更高版本。
-
已在你的项目中初始化shadcn/ui(执行命令:
npx shadcn@latest init)。 -
具备React和TypeScript的基本使用知识。
-
从右侧滑出的面板,其中包含购物车图标以及显示商品数量的信息
-
可单独调整每件商品的购买数量、减少数量或将其从购物车中移除
-
根据购物车中的商品信息,实时计算出小计和总金额
-
页面顶部和底部为固定内容,中间是可滚动显示的商品列表
-
从左侧滑出的面板
-
多选类别的复选框
-
价格范围滑块,可实时显示最低价和最高价
-
单选星级评分的筛选选项
-
会显示当前激活的筛选条件数量,并提供“清除所有”按钮,但点击该按钮不会关闭筛选面板
如果你还没有初始化shadcn/ui,请在项目根目录下运行npx shadcn@latest init,并按照提示完成相关设置后再继续下一步。
你将构建什么
本教程使用的组件都是通过Shadcn Space注册表来获取的。这个注册表是一个开源资源库,其中包含了许多适用于shadcn/ui的生产级组件和UI元素。
你可以在Shadcn Sheet组件库中查看所有这些组件。每个组件都同时支持Radix UI和Base UI的设计规范,并且都提供了复制代码选项,因此你在进行原型设计时可以将这些组件的配置信息直接粘贴到v0、Lovable或Bolt等开发工具中。本教程中使用的是Base UI版本的组件。
购物车页面模板(sheet-03)
筛选面板模板(sheet-04)
如何设置CLI注册表
在运行任何安装命令之前,请在项目的components.json文件中注册Shadcn Space注册表。
打开项目根目录下的components.json文件,添加registries字段:
{
"registries": {
"@shadcn-space": {
"url": "https://shadcnspace.com/r/{name}.json"
}
}
}
这样就能告诉shadcn CLI,哪些以@shadcn-space/为前缀的组件需要被加载。如果不执行这一步骤,后续的安装命令将会失败。
有关注册表设置的详细步骤,请参阅入门指南MCP服务器,因此如果你的工作流程需要使用MCP工具,就可以直接通过编辑器来浏览和安装组件。如果你更喜欢通过视频来学习,请观看以下链接:
如何构建购物车页面模板(sheet-03)
购物车页面的功能
购物车图标位于导航栏中。点击该图标后,会从右侧滑出一个面板,其中会显示每件商品的图片、名称、变体以及价格,并提供调整数量或完全删除商品的选项。随着你添加或删除商品,小计金额会实时更新;无论列表中包含多少商品,页面底部的信息始终会保持固定位置。
如何安装购物车页面模板
根据你使用的包管理器,运行以下命令之一:
npm:
npx shadcn@latest add @shadcn-space/sheet-03
pnpm:
pnpm dlx shadcn@latest add @shadcn-space/sheet-03
Yarn:
yarn dlx shadcn@latest add @shadcn-space/sheet-03
Bun:
bunx --bun shadcn@latest add @shadcn-space/sheet-03
命令行工具会将该组件复制到你的项目中,路径如下:
components/
shadcn-space/
sheet/
sheet-03.tsx
组件代码
"use client";
import { useState } from "react";
import { ShoppingCartIcon, PlusIcon, MinusIcon, Trash2Icon } from "lucide-react";
import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
SheetFooter,
SheetClose,
} from "@/components/ui/sheet";
import { Button } from "@components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@components/ui/button-group";
import { Badge } from "@components/ui/badge";
import { Separator } from "@components/ui/separator";
const initialItems = [
{
id: 1,
name: "Apple Watch S9",
variant: "Midnight / 41mm",
price: 684.0,
qty: 1,
image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-1.webp",
},
{
id: 2,
name: "Beige Jacket",
variant: "Size M / Beige",
price: 479.0,
qty: 1,
image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-02-2.webp",
},
{
id: 3,
name: "Glow Serum",
variant: "30ml / Vitamin C",
price: 46.0,
qty: 2,
image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-3.webp",
},
];
const ShoppingCartDemo = () => {
const [items, setItems] = useState(initialItems);
const updateQty = (id, delta) => {
setItems((prev) =>
prev
.map((item) => (item.id === id ? { ...item, qty: item.qty + delta } : item))
.filter((item) => itemqty > 0)
);
};
const subtotal = items.reduce((sum, item) => sum + item.price * item qty, 0);
const totalCount = items.reduce((sum, item) => sum + item.qty, 0);
return (
}>
0 && (
您的购物车
{totalCount > 0
? `${totalCount}件商品在您的购物车中`
: "您的购物车是空的"
>{item.name}</p>
>{item.variant}</p>
>${item.price.toFixed(2)}</p>
<\/Button>
{item qty}</ButtonGroupText>
))}






