Skip to content

Practical examples

This page shows how the Lovrabet SDK integrates into real projects, with recipes for React, Vue, Node.js, and other environments.

💡 Prerequisite Before you start, make sure the SDK is configured. Letting the CLI generate the configuration automatically is recommended.

The examples below use the alias style (client.models.users) for readability. The standard style (client.models.dataset_xxx) works identically.

⚛️ React integration

Project structure

Plain
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 configuration

TypeScript
// 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);
TypeScript
// 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);
}

Type definitions

TypeScript
// 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">
>;

Custom hook

TypeScript
// 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 component example

Plain
// 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>
              {user.status}
              {user.role}
            </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>
  );
};
Plain
// 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 integration

Composition API hook

TypeScript
// 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 component example

Plain
<!-- 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>
          {{ user.status }}
        </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 server integration

Express.js API server

TypeScript
// 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}`);
});

Scheduled task example

TypeScript
// 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 full-stack app

API routes

TypeScript
// 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`);
  }
}

Server-side rendering

Plain
// 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,
    };
  }
};

🔄 List sorting examples

Basic sorting

TypeScript
// 按创建时间降序获取用户列表
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;
};

Multi-field sorting

TypeScript
// 获取订单:优先级高的在前,相同优先级按创建时间排序
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;
};

Sorting in a React component

TypeScript
// 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>
  );
};

Sorting in a Vue component

Plain
<!-- 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>

Sorting in a Node.js API

TypeScript
// 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;

📥 Data export examples v1.1.24+

Excel export in React

Plain
// 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>
  );
};

Excel export in Vue 3

Plain
<!-- 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>
Plain
<!-- 在页面中使用 -->
<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>

Server-side export in Node.js

TypeScript
// 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;

Auto-downloading exported files

TypeScript
// 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>
  );
}

📖 Best practices recap

1. Error boundaries

TypeScript
// 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. Data validation

TypeScript
// 使用 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;
  }
};

📖 Next steps

With these examples in hand, continue with:

  • 🔧 Troubleshooting - common problems and debugging tips

We hope these examples help you integrate the Lovrabet SDK successfully into your project!

基于飞书知识库同步生成,内容以飞书源文档为准