Skip to content

实际应用示例

本文提供 Lovrabet SDK 在实际项目中的应用示例,包括 React、Vue、Node.js 等不同环境的集成方案。

💡 前置条件开始之前,请确保已完成 SDK 配置。推荐使用 CLI 自动生成配置。

以下示例使用别名方式 client.models.users 以便阅读,也可以使用标准方式 client.models.dataset_xxx(功能完全一致)。

⚛️ React 应用集成

项目结构设置

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 配置

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

类型定义

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

自定义 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 组件示例

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 应用集成

组合式 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 组件示例

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 服务端集成

Express.js API 服务器

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

定时任务示例

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 全栈应用

API 路由

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

服务端渲染

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

🔄 列表排序示例

基础排序使用

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;
};

多字段排序

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;
};

React 组件中使用排序

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

Vue 组件中使用排序

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>

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;

📥 数据导出示例 v1.1.24+

React 中使用 Excel 导出

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

Vue 3 中使用 Excel 导出

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>

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;

自动下载导出文件

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

📖 最佳实践总结

1. 错误边界处理

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. 数据验证

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;
  }
};

📖 下一步

查看实际应用示例后,您可以继续学习:

  • 🔧 故障排查 - 常见问题和调试技巧

希望这些示例能帮助您在项目中成功集成 Lovrabet SDK!

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