列表页:分页查询与筛选
本节你将学到
- 用
filter查询数据列表 - 实现关键词搜索
- 添加状态筛选
- 实现分页加载
预计时间:20 分钟
我们要做什么?
开发一个客户列表页面,功能包括:
- 展示客户数据(姓名、电话、公司、状态)
- 支持关键词搜索
- 支持按状态筛选
- 支持分页加载

第一步:了解数据结构
在写代码之前,我们需要先知道客户表有哪些字段。
方法一:用 AI 帮你查(推荐)
如果你配置了 MCP,直接问 AI:
你:帮我查一下客户表有哪些字段?
AI:找到 customer 数据集,字段如下:
- id: 客户ID(主键)
- name: 姓名
- phone: 电话
- company: 公司
- status: 状态(枚举:potential/active/lost)
- create_time: 创建时间
方法二:查看生成的配置文件
用 CLI 创建项目后,会自动生成 src/api/api.ts 文件,里面有所有数据集的配置。
// src/api/api.ts 中的内容示例
export const LOVRABET_MODELS_CONFIG = {
models: [
{
datasetCode: '8d2dcbae08b54bdd84c00be558ed48df',
tableName: 'customer',
name: '客户',
alias: 'customers' // 别名,方便记忆
}
]
};
第二步:写一个最简单的列表
先不考虑搜索和筛选,实现一个最简单的列表页面。
在 src/pages/ 目录下创建文件:
// src/pages/customer-list.tsx
/**
* Title: 客户列表
*/
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
export default function CustomerList() {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 获取客户列表
async function fetchCustomers() {
try {
const result = await lovrabetClient.models.customers.filter({
pageSize: 20, // 每页 20 条
});
setCustomers(result.tableData);
} catch (error) {
console.error('获取数据失败:', error);
} finally {
setLoading(false);
}
}
fetchCustomers();
}, []);
if (loading) {
return <div>加载中...</div>;
}
return (
<div>
<h1>客户列表</h1>
<table>
<thead>
<tr>
<th>姓名</th>
<th>电话</th>
<th>公司</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{customers.map((customer: any) => (
<tr key={customer.id}>
<td>{customer.name}</td>
<td>{customer.phone}</td>
<td>{customer.company || '-'}</td>
<td>{customer.status}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
代码解读
lovrabetClient.models.customers- 访问客户数据集(customers 是别名).filter()- 查询方法,返回分页数据result.tableData- 数据列表数组
验证效果
lovrabet start
打开浏览器访问 http://localhost:5173,应该能看到客户列表了。
第三步:添加搜索功能
现在我们来加搜索框,让用户可以搜索客户。
// src/pages/customer-list.tsx
/**
* Title: 客户列表
*/
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
export default function CustomerList() {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [keyword, setKeyword] = useState(""); // 新增:搜索关键词
useEffect(() => {
async function fetchCustomers() {
setLoading(true);
try {
const result = await lovrabetClient.models.customers.filter({
where: keyword
? {
// 搜索姓名、电话或公司(模糊匹配)
$or: [
{ name: { $contain: keyword } },
{ phone: { $contain: keyword } },
{ company: { $contain: keyword } },
],
}
: undefined,
pageSize: 20,
});
setCustomers(result.tableData);
} catch (error) {
console.error('获取数据失败:', error);
} finally {
setLoading(false);
}
}
fetchCustomers();
}, [keyword]); // keyword 变化时重新查询
return (
<div>
<h1>客户列表</h1>
{/* 新增:搜索框 */}
<input
type="text"
placeholder="搜索姓名、电话或公司..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ marginBottom: 16, padding: 8, width: 300 }}
/>
<table>
{/* ... 表格内容不变 ... */}
</table>
</div>
);
}
代码解读
$or- 或条件,满足任意一个即可$contain- 模糊匹配,相当于 SQL 的 LIKEuseEffect的依赖数组包含keyword,输入时会自动重新查询
第四步:添加状态筛选
除了搜索,我们还想按状态筛选客户。
// src/pages/customer-list.tsx
/**
* Title: 客户列表
*/
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
export default function CustomerList() {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [keyword, setKeyword] = useState("");
const [status, setStatus] = useState(""); // 新增:状态筛选
useEffect(() => {
async function fetchCustomers() {
setLoading(true);
try {
// 构建查询条件
const conditions: any[] = [];
// 状态筛选
if (status) {
conditions.push({ status: { $eq: status } });
}
// 关键词搜索
if (keyword) {
conditions.push({
$or: [
{ name: { $contain: keyword } },
{ phone: { $contain: keyword } },
{ company: { $contain: keyword } },
],
});
}
const result = await lovrabetClient.models.customers.filter({
where: conditions.length > 0 ? { $and: conditions } : undefined,
pageSize: 20,
});
setCustomers(result.tableData);
} catch (error) {
console.error('获取数据失败:', error);
} finally {
setLoading(false);
}
}
fetchCustomers();
}, [keyword, status]); // keyword 或 status 变化时重新查询
return (
<div>
<h1>客户列表</h1>
{/* 搜索和筛选 */}
<div style={{ marginBottom: 16 }}>
<input
type="text"
placeholder="搜索..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ padding: 8, width: 200 }}
/>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
style={{ marginLeft: 8, padding: 8 }}
>
<option value="">全部状态</option>
<option value="potential">潜在客户</option>
<option value="active">活跃客户</option>
<option value="lost">流失客户</option>
</select>
</div>
<table>
{/* ... 表格内容 ... */}
</table>
</div>
);
}
代码解读
$and- 与条件,所有条件都要满足$eq- 等于,精确匹配- 搜索和筛选同时生效时,用
$and连接
第五步:添加分页
数据量大了需要分页,我们来加分页功能。
// src/pages/customer-list.tsx
/**
* Title: 客户列表
*/
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
export default function CustomerList() {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [keyword, setKeyword] = useState("");
const [status, setStatus] = useState("");
// 新增:分页状态
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 20;
useEffect(() => {
async function fetchCustomers() {
setLoading(true);
try {
const conditions: any[] = [];
if (status) {
conditions.push({ status: { $eq: status } });
}
if (keyword) {
conditions.push({
$or: [
{ name: { $contain: keyword } },
{ phone: { $contain: keyword } },
{ company: { $contain: keyword } },
],
});
}
const result = await lovrabetClient.models.customers.filter({
where: conditions.length > 0 ? { $and: conditions } : undefined,
currentPage: page, // 当前页
pageSize: pageSize, // 每页条数
orderBy: [{ id: "desc" }], // 按ID倒序
});
setCustomers(result.tableData);
setTotal(result.total); // 总条数
} catch (error) {
console.error('获取数据失败:', error);
} finally {
setLoading(false);
}
}
fetchCustomers();
}, [keyword, status, page]); // page 变化时也要重新查询
// 搜索或筛选变化时,重置到第一页
useEffect(() => {
setPage(1);
}, [keyword, status]);
const totalPages = Math.ceil(total / pageSize);
return (
<div>
<h1>客户列表</h1>
{/* 搜索和筛选 */}
<div style={{ marginBottom: 16 }}>
<input
type="text"
placeholder="搜索..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ padding: 8, width: 200 }}
/>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
style={{ marginLeft: 8, padding: 8 }}
>
<option value="">全部状态</option>
<option value="potential">潜在客户</option>
<option value="active">活跃客户</option>
<option value="lost">流失客户</option>
</select>
</div>
{/* 数据表格 */}
<table>
<thead>
<tr>
<th>姓名</th>
<th>电话</th>
<th>公司</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={4}>加载中...</td></tr>
) : customers.length === 0 ? (
<tr><td colSpan={4}>暂无数据</td></tr>
) : (
customers.map((customer: any) => (
<tr key={customer.id}>
<td>{customer.name}</td>
<td>{customer.phone}</td>
<td>{customer.company || '-'}</td>
<td>{customer.status}</td>
</tr>
))
)}
</tbody>
</table>
{/* 新增:分页控件 */}
<div style={{ marginTop: 16 }}>
<span>共 {total} 条,第 {page}/{totalPages} 页</span>
<button
disabled={page <= 1}
onClick={() => setPage(p => p - 1)}
style={{ marginLeft: 8 }}
>
上一页
</button>
<button
disabled={page >= totalPages}
onClick={() => setPage(p => p + 1)}
style={{ marginLeft: 8 }}
>
下一页
</button>
</div>
</div>
);
}
代码解读
currentPage- 当前页码(从 1 开始)pageSize- 每页条数result.total- 符合条件的总记录数orderBy- 排序规则,desc降序,asc升序
完整代码
把上面的代码整合一下,完整的页面如下:
点击查看完整代码
// src/pages/customer-list.tsx
/**
* Title: 客户列表
*/
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
// 定义客户数据类型
interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
}
// 状态显示名称
const STATUS_LABELS: Record<string, string> = {
potential: "潜在客户",
active: "活跃客户",
lost: "流失客户",
};
export default function CustomerList() {
// 数据状态
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true);
// 筛选条件
const [keyword, setKeyword] = useState("");
const [status, setStatus] = useState("");
// 分页
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 20;
// 获取数据
useEffect(() => {
async function fetchCustomers() {
setLoading(true);
try {
// 构建查询条件
const conditions: any[] = [];
if (status) {
conditions.push({ status: { $eq: status } });
}
if (keyword) {
conditions.push({
$or: [
{ name: { $contain: keyword } },
{ phone: { $contain: keyword } },
{ company: { $contain: keyword } },
],
});
}
const result = await lovrabetClient.models.customers.filter({
where: conditions.length > 0 ? { $and: conditions } : undefined,
currentPage: page,
pageSize: pageSize,
orderBy: [{ create_time: "desc" }],
});
setCustomers(result.tableData || []);
setTotal(result.total || 0);
} catch (error) {
console.error("获取数据失败:", error);
} finally {
setLoading(false);
}
}
fetchCustomers();
}, [keyword, status, page]);
// 筛选变化时重置页码
useEffect(() => {
setPage(1);
}, [keyword, status]);
const totalPages = Math.ceil(total / pageSize);
return (
<div style={{ padding: 24 }}>
<h1>客户列表</h1>
{/* 搜索和筛选 */}
<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>
<input
type="text"
placeholder="搜索姓名、电话或公司..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ padding: 8, width: 240 }}
/>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
style={{ padding: 8 }}
>
<option value="">全部状态</option>
<option value="potential">潜在客户</option>
<option value="active">活跃客户</option>
<option value="lost">流失客户</option>
</select>
</div>
{/* 数据表格 */}
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ backgroundColor: "#f5f5f5" }}>
<th style={thStyle}>姓名</th>
<th style={thStyle}>电话</th>
<th style={thStyle}>公司</th>
<th style={thStyle}>状态</th>
<th style={thStyle}>创建时间</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={5} style={tdStyle}>加载中...</td>
</tr>
) : customers.length === 0 ? (
<tr>
<td colSpan={5} style={tdStyle}>暂无数据</td>
</tr>
) : (
customers.map((customer) => (
<tr key={customer.id}>
<td style={tdStyle}>{customer.name}</td>
<td style={tdStyle}>{customer.phone}</td>
<td style={tdStyle}>{customer.company || "-"}</td>
<td style={tdStyle}>{STATUS_LABELS[customer.status]}</td>
<td style={tdStyle}>
{new Date(customer.create_time).toLocaleDateString()}
</td>
</tr>
))
)}
</tbody>
</table>
{/* 分页 */}
<div style={{ marginTop: 16, display: "flex", alignItems: "center", gap: 8 }}>
<span>共 {total} 条</span>
<button disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
上一页
</button>
<span>
第 {page} / {totalPages || 1} 页
</span>
<button disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}>
下一页
</button>
</div>
</div>
);
}
// 简单的样式
const thStyle: React.CSSProperties = {
padding: 12,
textAlign: "left",
borderBottom: "1px solid #ddd",
};
const tdStyle: React.CSSProperties = {
padding: 12,
borderBottom: "1px solid #eee",
};
本节小结
你学会了:
| 知识点 | 说明 |
|---|---|
filter() | 查询数据列表 |
where | 查询条件 |
$eq | 等于 |
$contain | 模糊匹配 |
$and / $or | 逻辑连接 |
currentPage / pageSize | 分页参数 |
orderBy | 排序 |
常见问题
Q: 为什么用 customers 而不是 dataset_xxx?
customers 是别名,在 src/api/api.ts 中配置的,方便记忆。你也可以用标准方式:
// 标准方式(使用完整的 datasetCode)
lovrabetClient.models.dataset_8d2dcbae08b54bdd84c00be558ed48df.filter()
// 别名方式(更易读)
lovrabetClient.models.customers.filter()
Q: 搜索不生效怎么办?
检查几点:
- 字段名是否正确(是数据库字段名,不是中文显示名)
$contain不要写成$like- 确保数据库里有匹配的数据
Q: 分页总数不对?
result.total 返回的是符合筛选条件的总记录数,不是当前页的记录数。