跳到主要内容

列表页:分页查询与筛选

本节你将学到
  • 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 的 LIKE
  • useEffect 的依赖数组包含 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: 搜索不生效怎么办?

检查几点:

  1. 字段名是否正确(是数据库字段名,不是中文显示名)
  2. $contain 不要写成 $like
  3. 确保数据库里有匹配的数据

Q: 分页总数不对?

result.total 返回的是符合筛选条件的总记录数,不是当前页的记录数。


下一步