实际应用示例
本文提供 Lovrabet SDK 在实际项目中的应用示例,包括 React、Vue、Node.js 等不同环境的集成方案。
前置条件
开始之前,请确保已完成 SDK 配置。推荐使用 CLI 自动生成配置。
以下示例使用别名方式 client.models.users 以便阅读,也可以使用标准方式 client.models.dataset_xxx(功能完全一致)。
⚛️ React 应用集成
项目结构设置
src/
├── api/
│ ├── config.ts # API 配置
│ ├── client.ts # 客户端实例
│ └── types.ts # 类型定义
├── hooks/
│ ├── useLovrabet.ts # 自定义 Hook
│ └── useAuth.ts # 认证 Hook
├── components/
│ ├── UserList.tsx # 用户列表组件
│ ├── UserForm.tsx # 用户表单组件
│ └── Dashboard.tsx # 仪表盘组件
└── utils/
└── errorHandler.ts # 错误处理
API 配置
// src/api/config.ts
import { registerModels, type ModelsConfig } from "@lovrabet/sdk";
export const LOVRABET_CONFIG: ModelsConfig = {
appCode: process.env.REACT_APP_LOVRABET_APP_CODE!,
models: [
{
datasetCode: process.env.REACT_APP_USERS_DATASET_ID!,
tableName: "users",
alias: "users",
name: "用户管理",
},
{
datasetCode: process.env.REACT_APP_ORDERS_DATASET_ID!,
tableName: "orders",
alias: "orders",
name: "订单管理",
},
{
datasetCode: process.env.REACT_APP_PRODUCTS_DATASET_ID!,
tableName: "products",
alias: "products",
name: "产品管理",
},
],
} as const;
// 注册配置
registerModels(LOVRABET_CONFIG);
// src/api/client.ts
import { createClient } from "@lovrabet/sdk";
import "./config"; // 导入配置以执行注册
// 创建客户端实例
export const lovrabetClient = createClient({
options: {
timeout: 30000,
onError: (error) => {
console.error("Lovrabet API Error:", error);
// 全局错误处理
if (error.status === 401) {
// 清除用户认证状态
localStorage.removeItem("auth_token");
window.location.href = "/login";
}
},
},
});
// 设置认证 token
const token = localStorage.getItem("auth_token");
if (token) {
lovrabetClient.setToken(token);
}
类型定义
// src/api/types.ts
export interface User {
id: string;
name: string;
email: string;
status: "active" | "inactive" | "pending";
role: "admin" | "user" | "moderator";
avatar?: string;
createdAt: string;
updatedAt: string;
}
export interface Order {
id: string;
userId: string;
status: "pending" | "processing" | "completed" | "cancelled";
amount: number;
currency: string;
createdAt: string;
updatedAt: string;
}
export interface Product {
id: string;
name: string;
description?: string;
price: number;
inStock: boolean;
categoryId: string;
createdAt: string;
}
// 创建和更新类型
export type CreateUserData = Omit<User, "id" | "createdAt" | "updatedAt">;
export type UpdateUserData = Partial<
Omit<User, "id" | "createdAt" | "updatedAt">
>;
自定义 Hook
// src/hooks/useLovrabet.ts
import { useState, useEffect, useCallback } from "react";
import { lovrabetClient } from "../api/client";
import type { ListResponse, ListParams } from "@lovrabet/sdk";
interface UseModelListResult<T> {
data: T[] | null;
loading: boolean;
error: string | null;
pagination: {
current: number;
size: number;
total: number;
pages: number;
} | null;
refetch: (params?: ListParams) => Promise<void>;
loadMore: () => Promise<void>;
create: (data: any) => Promise<T>;
update: (id: string, data: any) => Promise<T>;
delete: (id: string) => Promise<void>;
}
export function useModelList<T>(
modelName: string,
initialParams: ListParams = {}
): UseModelListResult<T> {
const [data, setData] = useState<T[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] =
useState<UseModelListResult<T>["pagination"]>(null);
const [params, setParams] = useState(initialParams);
const fetchData = useCallback(
async (requestParams: ListParams = {}, append = false) => {
if (!append) setLoading(true);
setError(null);
try {
const response: ListResponse<T> = await lovrabetClient.models[
modelName
].filter({
...params,
...requestParams,
});
const newData = response.tableData;
setData((prevData) =>
append ? [...(prevData || []), ...newData] : newData
);
setPagination({
current: response.currentPage,
size: response.pageSize,
total: response.total,
pages: Math.ceil(response.total / response.pageSize),
});
} catch (err: any) {
const errorMessage = err?.message || "Failed to fetch data";
setError(errorMessage);
console.error(`Error fetching ${modelName}:`, err);
} finally {
setLoading(false);
}
},
[modelName, params]
);
const refetch = useCallback(
(newParams?: ListParams) => {
if (newParams) {
setParams((prev) => ({ ...prev, ...newParams }));
}
return fetchData(newParams);
},
[fetchData]
);
const loadMore = useCallback(async () => {
if (!pagination || pagination.current >= pagination.pages) return;
await fetchData(
{ ...params, currentPage: pagination.current + 1 },
true // append mode
);
}, [fetchData, params, pagination]);
const create = useCallback(
async (createData: any): Promise<T> => {
const newItem = await lovrabetClient.models[modelName].create<T>(
createData
);
setData((prevData) => [newItem, ...(prevData || [])]);
// 更新分页信息
setPagination((prev) =>
prev ? { ...prev, total: prev.total + 1 } : null
);
return newItem;
},
[modelName]
);
const update = useCallback(
async (id: string, updateData: any): Promise<T> => {
const updatedItem = await lovrabetClient.models[modelName].update<T>(
id,
updateData
);
setData((prevData) =>
prevData
? prevData.map((item) =>
(item as any).id === id ? updatedItem : item
)
: null
);
return updatedItem;
},
[modelName]
);
const deleteItem = useCallback(
async (id: string): Promise<void> => {
await lovrabetClient.models[modelName].delete(id);
setData((prevData) =>
prevData ? prevData.filter((item) => (item as any).id !== id) : null
);
// 更新分页信息
setPagination((prev) =>
prev ? { ...prev, total: prev.total - 1 } : null
);
},
[modelName]
);
useEffect(() => {
fetchData();
}, [fetchData]);
return {
data,
loading,
error,
pagination,
refetch,
loadMore,
create,
update,
delete: deleteItem,
};
}
// 特定模型的 Hook
export const useUsers = (params?: ListParams) =>
useModelList<User>("users", params);
export const useOrders = (params?: ListParams) =>
useModelList<Order>("orders", params);
export const useProducts = (params?: ListParams) =>
useModelList<Product>("products", params);
React 组件示例
// src/components/UserList.tsx
import React, { useState } from "react";
import { useUsers } from "../hooks/useLovrabet";
import type { User, CreateUserData, UpdateUserData } from "../api/types";
interface UserListProps {
filter?: Partial<User>;
onUserSelect?: (user: User) => void;
}
export const UserList: React.FC<UserListProps> = ({ filter, onUserSelect }) => {
const {
data: users,
loading,
error,
pagination,
refetch,
create,
update,
delete: deleteUser,
} = useUsers(filter);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const handleCreateUser = async (userData: CreateUserData) => {
try {
await create(userData);
setShowCreateForm(false);
} catch (error) {
console.error("Failed to create user:", error);
}
};
const handleUpdateUser = async (id: string, updates: UpdateUserData) => {
try {
await update(id, updates);
setEditingUser(null);
} catch (error) {
console.error("Failed to update user:", error);
}
};
const handleDeleteUser = async (id: string) => {
if (window.confirm("Are you sure you want to delete this user?")) {
try {
await deleteUser(id);
} catch (error) {
console.error("Failed to delete user:", error);
}
}
};
if (loading) return <div className="loading">Loading users...</div>;
if (error) return <div className="error">Error: {error}</div>;
if (!users?.length) return <div className="empty">No users found</div>;
return (
<div className="user-list">
<div className="header">
<h2>Users</h2>
<button onClick={() => setShowCreateForm(true)}>Add User</button>
</div>
{pagination && (
<div className="pagination-info">
Showing {users.length} of {pagination.total} users
</div>
)}
<div className="user-grid">
{users.map((user) => (
<div key={user.id} className="user-card">
<div className="user-info">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span className={`status ${user.status}`}>{user.status}</span>
<span className={`role ${user.role}`}>{user.role}</span>
</div>
<div className="user-actions">
<button onClick={() => onUserSelect?.(user)}>View</button>
<button onClick={() => setEditingUser(user)}>Edit</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="danger"
>
Delete
</button>
</div>
</div>
))}
</div>
{pagination && pagination.current < pagination.pages && (
<button
onClick={() => refetch({ currentPage: pagination.current + 1 })}
>
Load More
</button>
)}
{showCreateForm && (
<UserCreateModal
onSubmit={handleCreateUser}
onCancel={() => setShowCreateForm(false)}
/>
)}
{editingUser && (
<UserEditModal
user={editingUser}
onSubmit={(updates) => handleUpdateUser(editingUser.id, updates)}
onCancel={() => setEditingUser(null)}
/>
)}
</div>
);
};
// src/components/Dashboard.tsx
import React from "react";
import { useUsers, useOrders, useProducts } from "../hooks/useLovrabet";
export const Dashboard: React.FC = () => {
const { data: users, loading: usersLoading } = useUsers({ pageSize: 5 });
const { data: orders, loading: ordersLoading } = useOrders({ pageSize: 5 });
const { data: products, loading: productsLoading } = useProducts({
pageSize: 5,
});
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="dashboard-grid">
<div className="dashboard-card">
<h2>Recent Users</h2>
{usersLoading ? (
<div>Loading...</div>
) : (
<div>
{users?.slice(0, 5).map((user) => (
<div key={user.id} className="dashboard-item">
{user.name} - {user.status}
</div>
))}
</div>
)}
</div>
<div className="dashboard-card">
<h2>Recent Orders</h2>
{ordersLoading ? (
<div>Loading...</div>
) : (
<div>
{orders?.slice(0, 5).map((order) => (
<div key={order.id} className="dashboard-item">
Order #{order.id} - ${order.amount}
</div>
))}
</div>
)}
</div>
<div className="dashboard-card">
<h2>Products</h2>
{productsLoading ? (
<div>Loading...</div>
) : (
<div>
{products?.slice(0, 5).map((product) => (
<div key={product.id} className="dashboard-item">
{product.name} - ${product.price}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};
🟢 Vue 3 应用集成
组合式 API Hook
// src/composables/useLovrabet.ts
import { ref, computed, onMounted, type Ref } from "vue";
import { lovrabetClient } from "../api/client";
import type { ListResponse, ListParams } from "@lovrabet/sdk";
export function useModelList<T>(
modelName: string,
initialParams: ListParams = {}
) {
const data: Ref<T[]> = ref([]);
const loading = ref(true);
const error = ref<string | null>(null);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const pagination = computed(() => ({
current: currentPage.value,
size: pageSize.value,
total: total.value,
pages: Math.ceil(total.value / pageSize.value),
}));
const fetchData = async (params: ListParams = {}) => {
loading.value = true;
error.value = null;
try {
const response: ListResponse<T> = await lovrabetClient.models[
modelName
].filter({
...initialParams,
...params,
});
data.value = response.tableData;
total.value = response.total;
currentPage.value = response.currentPage;
pageSize.value = response.pageSize;
} catch (err: any) {
error.value = err?.message || "Failed to fetch data";
console.error(`Error fetching ${modelName}:`, err);
} finally {
loading.value = false;
}
};
const create = async (createData: any): Promise<T> => {
const newItem = await lovrabetClient.models[modelName].create<T>(
createData
);
data.value.unshift(newItem);
total.value += 1;
return newItem;
};
const update = async (id: string, updateData: any): Promise<T> => {
const updatedItem = await lovrabetClient.models[modelName].update<T>(
id,
updateData
);
const index = data.value.findIndex((item: any) => item.id === id);
if (index !== -1) {
data.value[index] = updatedItem;
}
return updatedItem;
};
const remove = async (id: string): Promise<void> => {
await lovrabetClient.models[modelName].delete(id);
data.value = data.value.filter((item: any) => item.id !== id);
total.value -= 1;
};
const refetch = (params?: ListParams) => fetchData(params);
onMounted(() => fetchData());
return {
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
pagination: readonly(pagination),
refetch,
create,
update,
remove,
};
}
// 特定模型的组合函数
export const useUsers = (params?: ListParams) => useModelList("users", params);
export const useOrders = (params?: ListParams) =>
useModelList("orders", params);
Vue 组件示例
<!-- src/components/UserList.vue -->
<template>
<div class="user-list">
<div class="header">
<h2>Users</h2>
<button @click="showCreateModal = true">Add User</button>
</div>
<div v-if="loading" class="loading">Loading users...</div>
<div v-else-if="error" class="error">Error: {{ error }}</div>
<div v-else-if="!data.length" class="empty">No users found</div>
<div v-else class="user-grid">
<div v-for="user in data" :key="user.id" class="user-card">
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span :class="`status ${user.status}`">{{ user.status }}</span>
</div>
<div class="user-actions">
<button @click="$emit('userSelect', user)">View</button>
<button @click="editUser(user)">Edit</button>
<button @click="deleteUser(user.id)" class="danger">Delete</button>
</div>
</div>
</div>
<div v-if="pagination.pages > 1" class="pagination">
<button
v-for="page in pagination.pages"
:key="page"
@click="refetch({ currentPage: page })"
:class="{ active: page === pagination.current }"
>
{{ page }}
</button>
</div>
<!-- 创建用户模态框 -->
<UserCreateModal
v-if="showCreateModal"
@submit="handleCreateUser"
@cancel="showCreateModal = false"
/>
<!-- 编辑用户模态框 -->
<UserEditModal
v-if="editingUser"
:user="editingUser"
@submit="handleUpdateUser"
@cancel="editingUser = null"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsers } from "../composables/useLovrabet";
import type { User, CreateUserData, UpdateUserData } from "../api/types";
interface Props {
filter?: Partial<User>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
userSelect: [user: User];
}>();
const { data, loading, error, pagination, refetch, create, update, remove } =
useUsers(props.filter);
const showCreateModal = ref(false);
const editingUser = ref<User | null>(null);
const handleCreateUser = async (userData: CreateUserData) => {
try {
await create(userData);
showCreateModal.value = false;
} catch (error) {
console.error("Failed to create user:", error);
}
};
const editUser = (user: User) => {
editingUser.value = user;
};
const handleUpdateUser = async (updates: UpdateUserData) => {
if (!editingUser.value) return;
try {
await update(editingUser.value.id, updates);
editingUser.value = null;
} catch (error) {
console.error("Failed to update user:", error);
}
};
const deleteUser = async (id: string) => {
if (confirm("Are you sure you want to delete this user?")) {
try {
await remove(id);
} catch (error) {
console.error("Failed to delete user:", error);
}
}
};
</script>
🚀 Node.js 服务端集成
Express.js API 服务器
// src/server.ts
import express from "express";
import cors from "cors";
import { registerModels, createClient } from "@lovrabet/sdk";
// 配置 Lovrabet SDK
registerModels({
appCode: process.env.LOVRABET_APP_CODE!,
models: [
{
datasetCode: process.env.USERS_DATASET_ID!,
tableName: "users",
alias: "users",
},
{
datasetCode: process.env.ORDERS_DATASET_ID!,
tableName: "orders",
alias: "orders",
},
],
});
// 创建认证客户端
const lovrabetClient = createClient({
accessKey: process.env.LOVRABET_ACCESS_KEY!,
secretKey: process.env.LOVRABET_SECRET_KEY!,
});
const app = express();
app.use(cors());
app.use(express.json());
// 用户路由
app.get("/api/users", async (req, res) => {
try {
const { page = 1, size = 20, status } = req.query;
const params = {
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
...(status && { status }),
};
const users = await lovrabetClient.models.users.filter(params);
res.json({
success: true,
data: users.tableData,
pagination: {
current: users.currentPage,
size: users.pageSize,
total: users.total,
pages: Math.ceil(users.total / users.pageSize),
},
});
} catch (error: any) {
console.error("Error fetching users:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.post("/api/users", async (req, res) => {
try {
const userData = req.body;
// 数据验证
if (!userData.name || !userData.email) {
return res.status(400).json({
success: false,
error: "Name and email are required",
});
}
const newUser = await lovrabetClient.models.users.create(userData);
res.status(201).json({
success: true,
data: newUser,
});
} catch (error: any) {
console.error("Error creating user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.put("/api/users/:id", async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const updatedUser = await lovrabetClient.models.users.update(id, updates);
res.json({
success: true,
data: updatedUser,
});
} catch (error: any) {
console.error("Error updating user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.delete("/api/users/:id", async (req, res) => {
try {
const { id } = req.params;
await lovrabetClient.models.users.delete(id);
res.json({
success: true,
message: "User deleted successfully",
});
} catch (error: any) {
console.error("Error deleting user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// 订单统计 API
app.get("/api/analytics/orders", async (req, res) => {
try {
const { startDate, endDate, userId } = req.query;
const params = {
currentPage: 1,
pageSize: 1000, // 获取更多数据用于统计
...(userId && { userId }),
...(startDate && { createdAfter: startDate }),
...(endDate && { createdBefore: endDate }),
};
const ordersResponse = await lovrabetClient.models.orders.filter(params);
const orders = ordersResponse.tableData;
// 计算统计信息
const stats = {
totalOrders: orders.length,
totalAmount: orders.reduce((sum, order) => sum + (order.amount || 0), 0),
averageAmount: 0,
statusBreakdown: {} as Record<string, number>,
dailyStats: {} as Record<string, { orders: number; amount: number }>,
};
stats.averageAmount =
stats.totalOrders > 0 ? stats.totalAmount / stats.totalOrders : 0;
// 状态分布
orders.forEach((order) => {
stats.statusBreakdown[order.status] =
(stats.statusBreakdown[order.status] || 0) + 1;
// 按日期分组
const date = new Date(order.createdAt).toISOString().split("T")[0];
if (!stats.dailyStats[date]) {
stats.dailyStats[date] = { orders: 0, amount: 0 };
}
stats.dailyStats[date].orders += 1;
stats.dailyStats[date].amount += order.amount || 0;
});
res.json({
success: true,
data: stats,
});
} catch (error: any) {
console.error("Error fetching order analytics:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
定时任务示例
// src/jobs/dataSync.ts
import cron from "node-cron";
import { lovrabetClient } from "../api/client";
// 每小时同步用户数据
cron.schedule("0 * * * *", async () => {
console.log("Starting hourly user data sync...");
try {
// 获取最近更新的用户
const response = await lovrabetClient.models.users.filter({
updatedAfter: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // 1小时前
pageSize: 100,
});
console.log(`Found ${response.tableData.length} users to sync`);
// 处理同步逻辑
for (const user of response.tableData) {
await syncUserToExternalSystem(user);
}
console.log("User data sync completed successfully");
} catch (error) {
console.error("Error during user data sync:", error);
}
});
// 每日生成报告
cron.schedule("0 9 * * *", async () => {
console.log("Generating daily report...");
try {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const startOfDay = new Date(yesterday.setHours(0, 0, 0, 0));
const endOfDay = new Date(yesterday.setHours(23, 59, 59, 999));
// 获取昨日订单
const ordersResponse = await lovrabetClient.models.orders.filter({
createdAfter: startOfDay.toISOString(),
createdBefore: endOfDay.toISOString(),
pageSize: 1000,
});
// 生成报告
const report = {
date: yesterday.toISOString().split("T")[0],
totalOrders: ordersResponse.total,
totalAmount: ordersResponse.tableData.reduce(
(sum, order) => sum + order.amount,
0
),
statusBreakdown: {},
};
console.log("Daily report generated:", report);
// 发送报告到监控系统或邮件
await sendDailyReport(report);
} catch (error) {
console.error("Error generating daily report:", error);
}
});
async function syncUserToExternalSystem(user: any) {
// 同步到外部系统的逻辑
console.log(`Syncing user ${user.id} to external system`);
}
async function sendDailyReport(report: any) {
// 发送报告的逻辑
console.log("Sending daily report:", report);
}
📱 Next.js 全栈应用
API 路由
// pages/api/users/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { lovrabetClient } from "../../../lib/lovrabet";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
switch (req.method) {
case "GET":
try {
const { page = 1, size = 20, ...filters } = req.query;
const response = await lovrabetClient.models.users.filter({
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
...filters,
});
res.status(200).json(response);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
break;
case "POST":
try {
const userData = req.body;
const newUser = await lovrabetClient.models.users.create(userData);
res.status(201).json(newUser);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
break;
default:
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
服务端渲染
// pages/users/[id].tsx
import { GetServerSideProps } from "next";
import { lovrabetClient } from "../../lib/lovrabet";
import type { User } from "../../types/api";
interface UserDetailProps {
user: User;
orders: any[];
}
export default function UserDetail({ user, orders }: UserDetailProps) {
return (
<div>
<h1>User: {user.name}</h1>
<p>Email: {user.email}</p>
<p>Status: {user.status}</p>
<h2>Orders ({orders.length})</h2>
{orders.map((order) => (
<div key={order.id}>
Order #{order.id} - ${order.amount}
</div>
))}
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
try {
// 并行获取用户信息和订单
const [user, ordersResponse] = await Promise.all([
lovrabetClient.models.users.getOne(id as string),
lovrabetClient.models.orders.filter({
userId: id as string,
pageSize: 50,
}),
]);
return {
props: {
user,
orders: ordersResponse.tableData,
},
};
} catch (error) {
return {
notFound: true,
};
}
};
🔄 列表排序示例
基础排序使用
// 按创建时间降序获取用户列表
const getRecentUsers = async () => {
const response = await lovrabetClient.models.users.filter(
{ currentPage: 1, pageSize: 20 },
[{ createTime: "desc" }]
);
return response.tableData;
};
// 按名称升序获取产品列表
const getProductsByName = async () => {
const response = await lovrabetClient.models.products.filter(
{ currentPage: 1, pageSize: 50 },
[{ name: "asc" }]
);
return response.tableData;
};
多字段排序
// 获取订单:优先级高的在前,相同优先级按创建时间排序
const getPrioritizedOrders = async () => {
const response = await lovrabetClient.models.orders.filter(
{ currentPage: 1, pageSize: 100 },
[
{ priority: "desc" }, // 优先级降序(高优先级在前)
{ createTime: "desc" }, // 创建时间降序(新的在前)
]
);
return response.tableData;
};
// 获取学生列表:按班级、成绩排序
const getStudentsByClass = async (grade: number) => {
const response = await lovrabetClient.models.students.filter(
{
grade, // 筛选年级
currentPage: 1,
pageSize: 50,
},
[
{ className: "asc" }, // 班级升序
{ score: "desc" }, // 成绩降序
{ name: "asc" }, // 姓名升序
]
);
return response.tableData;
};
React 组件中使用排序
// components/SortableUserList.tsx
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
import { SortOrder } from "@lovrabet/sdk";
interface User {
id: string;
name: string;
email: string;
createTime: string;
lastLoginTime: string;
}
export const SortableUserList = () => {
const [users, setUsers] = useState<User[]>([]);
const [sortField, setSortField] = useState<string>("createTime");
const [sortOrder, setSortOrder] = useState(SortOrder.DESC);
const [loading, setLoading] = useState(false);
const loadUsers = async () => {
setLoading(true);
try {
const response = await lovrabetClient.models.users.filter(
{ currentPage: 1, pageSize: 20 },
[{ [sortField]: sortOrder }]
);
setUsers(response.tableData);
} catch (error) {
console.error("加载用户失败:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadUsers();
}, [sortField, sortOrder]);
const handleSort = (field: string) => {
if (field === sortField) {
// 切换排序方向
setSortOrder((prev) => (prev === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC));
} else {
// 新字段,默认降序
setSortField(field);
setSortOrder(SortOrder.DESC);
}
};
return (
<div className="user-list">
<table>
<thead>
<tr>
<th onClick={() => handleSort("name")}>
姓名 {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("email")}>
邮箱 {sortField === "email" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("createTime")}>
创建时间{" "}
{sortField === "createTime" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("lastLoginTime")}>
最后登录{" "}
{sortField === "lastLoginTime" &&
(sortOrder === "asc" ? "↑" : "↓")}
</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={4}>加载中...</td>
</tr>
) : (
users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{new Date(user.createTime).toLocaleString()}</td>
<td>{new Date(user.lastLoginTime).toLocaleString()}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
};
Vue 组件中使用排序
<!-- components/SortableProductList.vue -->
<template>
<div class="product-list">
<div class="sort-controls">
<label>
排序字段:
<select v-model="sortField">
<option value="name">产品名称</option>
<option value="price">价格</option>
<option value="sales">销量</option>
<option value="createTime">创建时间</option>
</select>
</label>
<label>
排序方向:
<select v-model="sortOrder">
<option :value="SortOrder.ASC">升序</option>
<option :value="SortOrder.DESC">降序</option>
</select>
</label>
</div>
<div v-if="loading">加载中...</div>
<div v-else class="products">
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p>价格: ¥{{ product.price }}</p>
<p>销量: {{ product.sales }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { SortOrder, type SortList } from "@lovrabet/sdk";
import { lovrabetClient } from "@/api/client";
interface Product {
id: string;
name: string;
price: number;
sales: number;
createTime: string;
}
const products = ref<Product[]>([]);
const loading = ref(false);
const sortField = ref("sales");
const sortOrder = ref(SortOrder.DESC);
const loadProducts = async () => {
loading.value = true;
try {
const sortList: SortList = [{ [sortField.value]: sortOrder.value }];
const response = await lovrabetClient.models.products.filter(
{ currentPage: 1, pageSize: 50 },
sortList
);
products.value = response.tableData;
} catch (error) {
console.error("加载产品失败:", error);
} finally {
loading.value = false;
}
};
// 监听排序变化
watch([sortField, sortOrder], loadProducts, { immediate: true });
</script>
Node.js API 中使用排序
// routes/api.ts
import express from "express";
import { SortOrder, type SortList } from "@lovrabet/sdk";
import { lovrabetClient } from "../config/lovrabet";
const router = express.Router();
// 获取用户列表(支持排序)
router.get("/users", async (req, res) => {
try {
const { page = 1, size = 20, sortBy, order } = req.query;
// 构建排序列表
let sortList: SortList | undefined;
if (sortBy && order) {
const sortOrder = order === "asc" ? SortOrder.ASC : SortOrder.DESC;
sortList = [{ [sortBy as string]: sortOrder }];
}
const response = await lovrabetClient.models.users.filter(
{
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
},
sortList
);
res.json({
success: true,
data: response.tableData,
total: response.total,
page: response.currentPage,
});
} catch (error) {
res.status(500).json({
success: false,
error: "获取用户列表失败",
});
}
});
// 获取热门产品(复杂排序)
router.get("/products/trending", async (req, res) => {
try {
const response = await lovrabetClient.models.products.filter(
{
status: "active",
currentPage: 1,
pageSize: 20,
},
[
{ sales: SortOrder.DESC }, // 销量降序
{ rating: SortOrder.DESC }, // 评分降序
{ createTime: SortOrder.DESC }, // 创建时间降序
]
);
res.json({
success: true,
data: response.tableData,
});
} catch (error) {
res.status(500).json({
success: false,
error: "获取热门产品失败",
});
}
});
export default router;
📥 数据导出示例 v1.1.24+
React 中使用 Excel 导出
// src/components/UserExportButton.tsx
import React, { useState } from "react";
import { lovrabetClient } from "../api/client";
import type { ListParams } from "@lovrabet/sdk";
interface ExportButtonProps {
filters?: ListParams;
buttonText?: string;
}
export const UserExportButton: React.FC<ExportButtonProps> = ({
filters = {},
buttonText = "导出 Excel",
}) => {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleExport = async () => {
setExporting(true);
setError(null);
try {
// 调用 excelExport 导出数据
const fileUrl = await lovrabetClient.models.users.excelExport(filters);
// 在新窗口打开下载链接
window.open(fileUrl, "_blank");
console.log("导出成功:", fileUrl);
} catch (err: any) {
const errorMessage = err?.message || "导出失败";
setError(errorMessage);
console.error("导出失败:", err);
} finally {
setExporting(false);
}
};
return (
<div className="export-container">
<button onClick={handleExport} disabled={exporting} className="export-btn">
{exporting ? "导出中..." : buttonText}
</button>
{error && <div className="error-message">错误: {error}</div>}
</div>
);
};
// 使用示例:在用户列表页面中添加导出按钮
export const UserListWithExport: React.FC = () => {
const [filters, setFilters] = useState<ListParams>({
status: "active",
});
return (
<div className="user-list-page">
<div className="toolbar">
<h2>用户列表</h2>
{/* 导出所有活跃用户 */}
<UserExportButton filters={filters} buttonText="导出活跃用户" />
{/* 导出所有数据(无筛选条件) */}
<UserExportButton buttonText="导出全部用户" />
</div>
<UserList filters={filters} />
</div>
);
};
Vue 3 中使用 Excel 导出
<!-- src/components/DataExportButton.vue -->
<template>
<div class="export-container">
<button
@click="handleExport"
:disabled="exporting"
class="export-btn"
>
{{ exporting ? '导出中...' : buttonText }}
</button>
<div v-if="error" class="error-message">
错误: {{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { lovrabetClient } from "@/api/client";
import type { ListParams } from "@lovrabet/sdk";
interface Props {
modelName: string;
filters?: ListParams;
buttonText?: string;
}
const props = withDefaults(defineProps<Props>(), {
buttonText: "导出 Excel",
filters: () => ({}),
});
const exporting = ref(false);
const error = ref<string | null>(null);
const handleExport = async () => {
exporting.value = true;
error.value = null;
try {
// 调用 excelExport 导出数据
const fileUrl = await lovrabetClient.models[props.modelName].excelExport(
props.filters
);
// 在新窗口打开下载链接
window.open(fileUrl, "_blank");
console.log("导出成功:", fileUrl);
} catch (err: any) {
error.value = err?.message || "导出失败";
console.error("导出失败:", err);
} finally {
exporting.value = false;
}
};
</script>
<style scoped>
.export-btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.export-btn:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
.error-message {
color: #ff4d4f;
margin-top: 8px;
font-size: 14px;
}
</style>
<!-- 在页面中使用 -->
<template>
<div class="user-list-page">
<div class="toolbar">
<h2>订单列表</h2>
<!-- 导出当前筛选条件的数据 -->
<DataExportButton
model-name="orders"
:filters="currentFilters"
button-text="导出当前订单"
/>
<!-- 导出所有数据 -->
<DataExportButton
model-name="orders"
button-text="导出全部订单"
/>
</div>
<OrderList />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import DataExportButton from "@/components/DataExportButton.vue";
const currentFilters = ref({
status: "completed",
createTime: "2025-01-01",
});
</script>
Node.js 服务端导出
// src/routes/export.ts
import express from "express";
import { lovrabetClient } from "../config/lovrabet";
const router = express.Router();
// 导出用户数据
router.get("/api/export/users", async (req, res) => {
try {
const { status, role, startDate, endDate } = req.query;
// 构建筛选条件
const filters: any = {};
if (status) filters.status = status;
if (role) filters.role = role;
if (startDate) filters.createdAfter = startDate;
if (endDate) filters.createdBefore = endDate;
// 导出数据
const fileUrl = await lovrabetClient.models.users.excelExport(filters);
// 返回文件 URL
res.json({
success: true,
fileUrl,
message: "导出成功",
});
} catch (error: any) {
console.error("导出失败:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// 批量导出多个数据集
router.post("/api/export/batch", async (req, res) => {
try {
const { exports } = req.body; // { exports: [{ model: 'users', filters: {...} }] }
const results = await Promise.all(
exports.map(async ({ model, filters }) => {
const fileUrl = await lovrabetClient.models[model].excelExport(filters);
return {
model,
fileUrl,
success: true,
};
})
);
res.json({
success: true,
results,
message: `成功导出 ${results.length} 个文件`,
});
} catch (error: any) {
console.error("批量导出失败:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
export default router;
自动下载导出文件
// utils/downloadHelper.ts
/**
* 自动下载文件(通过创建临时 a 标签)
*/
export async function downloadExportedFile(
fileUrl: string,
filename: string = "export.xlsx"
): Promise<void> {
// 方式 1: 直接使用 window.open(推荐)
window.open(fileUrl, "_blank");
// 方式 2: 使用 fetch + Blob(需要处理 CORS)
// try {
// const response = await fetch(fileUrl);
// const blob = await response.blob();
//
// const link = document.createElement('a');
// link.href = URL.createObjectURL(blob);
// link.download = filename;
// link.click();
//
// // 释放 URL 对象
// URL.revokeObjectURL(link.href);
// } catch (error) {
// console.error('下载失败:', error);
// throw error;
// }
}
// React Hook: 使用导出功能
import { useState, useCallback } from "react";
import { lovrabetClient } from "../api/client";
import { downloadExportedFile } from "../utils/downloadHelper";
export function useExport(modelName: string) {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState<string | null>(null);
const exportData = useCallback(
async (filters = {}, filename?: string) => {
setExporting(true);
setError(null);
try {
const fileUrl = await lovrabetClient.models[modelName].excelExport(
filters
);
await downloadExportedFile(fileUrl, filename);
return fileUrl;
} catch (err: any) {
const errorMessage = err?.message || "导出失败";
setError(errorMessage);
throw err;
} finally {
setExporting(false);
}
},
[modelName]
);
return {
exporting,
error,
exportData,
};
}
// 使用 Hook
function UserListPage() {
const { exporting, error, exportData } = useExport("users");
const handleExport = async () => {
try {
await exportData(
{ status: "active" },
`users_${new Date().toISOString().split("T")[0]}.xlsx`
);
alert("导出成功!");
} catch (error) {
alert("导出失败,请重试");
}
};
return (
<button onClick={handleExport} disabled={exporting}>
{exporting ? "导出中..." : "导出用户"}
</button>
);
}
📖 最佳实践总结
1. 错误边界处理
// React 错误边界
class ApiErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: any) {
return { hasError: true, error };
}
componentDidCatch(error: any, errorInfo: any) {
console.error("API Error Boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>Reload Page</button>
</div>
);
}
return this.props.children;
}
}
2. 数据验证
// 使用 zod 进行数据验证
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
status: z.enum(["active", "inactive", "pending"]),
role: z.enum(["admin", "user", "moderator"]),
});
const validateAndCreateUser = async (userData: unknown) => {
try {
const validatedData = UserSchema.parse(userData);
return await lovrabetClient.models.users.create(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Validation failed: ${error.errors.map((e) => e.message).join(", ")}`
);
}
throw error;
}
};
📖 下一步
查看实际应用示例后,您可以继续学习:
- 🔧 故障排查 - 常见问题和调试技巧
希望这些示例能帮助您在项目中成功集成 Lovrabet SDK!