如何使用 NextJS、Supabase 和 Tailwind CSS 构建自定义 MedusaJS 管理仪表板

在管理电子商务业务时,没有两个业务需求是相同的。例如,一家电子商务商店可能需要一个专注于跟踪实时库存的仪表板,而另一家则优先考虑可视化销售趋势和客户参与度。这就是为什么现成的管理仪表板或预建模板经常不尽人意:它们不是为满足个别企业的特定需求而设计的。

然而,像 Medusa 这样的可定制解决方案通过提供构建模块和 REST API 端点来解决这个问题,这些模块和端点是构建可适应业务当前需求的自定义电子商务解决方案所需的。这可确保您能够控制电子商务生态系统的各个方面。

在本文中,你将了解如何:

  • 使用 MedusaJS 和 Supabase 设置电子商务后端。
  • 使用 Next.js 和 Tailwind CSS 构建自定义销售仪表板。
  • 扩展 MedusaJS 的管理功能以满足特定的业务需求。
  • 在本指南结束时,您将拥有一个如下所示的管理仪表板:

    Custom Admin Dashboard with MedusaJS

    先决条件

    要学习本教程,您需要满足以下条件:

  • Next.js 和 PostgreSQL 的基础知识
  • Node.js(v16 或更高版本)— 从 nodejs.org 安装。
  • 在本地设置您的 Medusa 商店

    请按照以下步骤设置您的 Medusa 商店:

    步骤 1:使用 Supabase 创建 PostgreSQL 数据库

  • 访问 Supabase 并注册。
  • 单击“开始您的项目”并创建一个新帐户。
  • 在仪表板上单击“新项目”
  • 填写数据库的详细信息,即项目名称和数据库密码。您可以使用默认区域,也可以选择离您较近的区域。
  • 之后,点击“创建新项目”。它将创建一个包含整个 Postgres 数据库的新 Supabase 项目。
  • Supabase

    第 2 步:获取数据库连接字符串

  • 导航到项目设置>连接。
  • Supabase Sign up Dashboard
  • 您将在“连接字符串”>“URL”下找到连接字符串。保存它 — 您将需要它来进行 Medusa 设置。
  • Supabase Dashboard

    步骤 3:在本地安装并设置 Medusa

    运行以下命令来安装并设置您的 Medusa 项目,并将其连接到您的 Supabase 数据库:

    npx create-medusa-app@latest - seed - db-url postgresql://postgres:@.supabase.co:5432/postgres

    替换 ``,并使用您在**步骤 1** 中创建的数据库密码。

    **命令标志解释:**

  • — seed:用演示数据为数据库播种。
  • — db-url:指定您的数据库连接 URL。
  • 您可以在文档中看到 create-medusa-app 接受的其他 CLI 选项。

    安装完成后,您的项目将在以下端口提供服务:

  • Medusa 后端:http://localhost:9000
  • Medusa 管理仪表板:http://localhost:7001
  • supabase

    要访问管理仪表板,请创建一个管理员帐户或使用默认凭据(电子邮件:admin@medusa-test.com 密码:“supersecret”)

    Supabase

    添加自定义销售概览页面。

    除了默认功能外,MedusaJS 还允许您向管理仪表板添加自定义路线,从而让您能够跟踪特定的业务指标,例如销售业绩。以下是添加自定义**销售概览**页面的方法:

    1. 创建自定义管理界面路由

    MedusaJS 管理路由是 `src/admin/routes` 目录中的 React 组件。要为销售创建自定义路由:

  • 导航到项目目录中的 src/admin/routes。
  • 创建文件夹结构:sales/page.tsx。└── my-store/└── src/└── admin/└── routes/└── sales/└── page.tsx
  • 在 page.tsx 中,为销售页面导出一个 React 组件:
  • const Sales = () => {
      return (
        
    See the number of products sold and the number remaining in stock
    ) } export default Sales

    访问 http://localhost:7001/a/sales 查看您的自定义页面。但是,无法从侧边栏访问;您将在下一节中修复此问题。

    MedusaJS

    2. 将路线添加到侧边栏

    要使路线可从侧边栏访问:

  • 从@medusajs/admin 导入 RouteConfig 并定义路线配置:
  • import { RouteConfig } from "@medusajs/admin"
    import { CircleStack } from '@medusajs/icons';
    
    const Sales = () => {
      return (
        
    See the number of product sold and the number left in stock
    ) } export const config: RouteConfig = { link: { label: "Sales", icon: CircleStack, }, } export default Sales

    **销售**路线现在将出现在管理仪表板侧栏上。

    Sales route

    扩展产品实体以添加自定义字段

    要使**销售概览**页面正常运行,您需要确保您的数据库支持所有必需的数据。具体来说,您需要显示每种产品的**当前库存**、**价格**和**销售单位**。虽然前两个在默认架构中可用,但`units_sold`数据不可用。那么,如何添加自定义字段?

    步骤 1:查看当前架构

    首先探索现有的数据结构。您可以向 `/admin/products` 端点发出 `GET` 请求,以查看每个产品的可用字段。此端点可通过以下方式访问:

    http://localhost:9000/admin/products

    您会注意到没有“units_sold”列。这就是需要自定义的地方。

    第 2 步:将数据库架构修改为 Units_sold 列

    由于 Medusa 使用 **PostgreSQL 数据库** 并以 TypeORM 作为 ORM,因此您需要更新数据库模式和 MedusaJS 产品实体模型以添加新列。

    首先,修改数据库架构以包含“units_sold”列。为了获得更好的粒度,您可以添加季度列(“units_sold_q1”、“units_sold_q2”等)。直接在数据库上运行以下 SQL 命令:

    ALTER TABLE product ADD COLUMN units_sold_q1 INT DEFAULT 0;
    ALTER TABLE product ADD COLUMN units_sold_q2 INT DEFAULT 0;
    ALTER TABLE product ADD COLUMN units_sold_q3 INT DEFAULT 0;
    ALTER TABLE product ADD COLUMN units_sold_q4 INT DEFAULT 0;

    💡 运行这些命令后,新列将存在于您的数据库中,但尚未出现在 API 响应或管理仪表板中。要解决此问题,您需要更新 MedusaJS 实体模型。

    步骤 3:扩展产品实体模型

    Medusa 使用 TypeORM 来定义其模型。要将新列添加到产品实体,请在 `./src/models/` 中创建一个名为 product.ts 的文件:

    import { Column, Entity } from "typeorm";
        import { Product as MedusaProduct } from "@medusajs/medusa";
        @Entity()
        export class Product extends MedusaProduct {
        @Column({ default: 0 })
        units_sold_q1: number;
        @Column({ default: 0 })
        units_sold_q2: number;
        @Column({ default: 0 })
        units_sold_q3: number;
        @Column({ default: 0 })
        units_sold_q4: number;
        }

    `@Column` 装饰器确保 TypeORM 将字段映射到您的数据库。

    步骤 4:更新类型定义

    如果您使用的是 TypeScript,请通过扩展 Medusa 的核心“Product”接口将新列添加到 IDE 的自动完成功能中。在“src/index.d.ts”处创建一个文件:

    export declare module "@medusajs/medusa/dist/models/product" {
        declare interface Product {
        units_sold_q1: number;
        units_sold_q2: number;
        units_sold_q3: number;
        units_sold_q4: number;
         }
        }

    步骤 5:在 API 中公开新字段

    Medusa 的 API 默认不会返回您的自定义列。要包含这些字段,您需要修改产品端点配置。在 `src/loaders/extend-product-fields.ts` 处创建一个文件:

    export default async function () {
        const imports = await import(
        "@medusajs/medusa/dist/api/routes/admin/products/index"
        ) as any;
        imports.defaultAdminProductFields = [
        …imports.defaultAdminProductFields,
        "units_sold_q1",
        "units_sold_q2",
        "units_sold_q3",
        "units_sold_q4",
         ];
        };

    然后,通过将其添加到“medusa-config.js”中的“loaders”数组中,在您的Medusa项目中注册该加载器。

    步骤 6:创建 TypeORM 数据源

    要将新字段与数据库同步,请在 Medusa 项目的根目录下创建一个 TypeORM 数据源文件。将其保存为“datasource.js”:

    const { DataSource } = require("typeorm");
        const AppDataSource = new DataSource({
        type: "postgres",
        port: 5432,
        username: "",
        password: "",
        database: "",
        entities: ["dist/models/*.js"],
        migrations: ["dist/migrations/*.js"],
        });
        module.exports = {
        datasource: AppDataSource,
        };

    步骤 7:编写并运行迁移

    要以编程方式更新数据库模式,请创建并执行迁移:

  • 生成迁移文件:
  • npm run build
    npx typeorm migration:create src/migrations/AddUnitsSoldColumnToProduct
  • 编辑生成的迁移文件:
  • import { MigrationInterface, QueryRunner } from "typeorm";
    
    export class AddUnitsSoldColumnToProduct implements MigrationInterface {
      public async up(queryRunner: QueryRunner): Promise {
        await queryRunner.query(`
          ALTER TABLE product ADD COLUMN units_sold_q1 INT DEFAULT 0;
          ALTER TABLE product ADD COLUMN units_sold_q2 INT DEFAULT 0;
          ALTER TABLE product ADD COLUMN units_sold_q3 INT DEFAULT 0;
          ALTER TABLE product ADD COLUMN units_sold_q4 INT DEFAULT 0;
        `);
      }
    
      public async down(queryRunner: QueryRunner): Promise {
        await queryRunner.query(`
          ALTER TABLE product DROP COLUMN units_sold_q1;
          ALTER TABLE product DROP COLUMN units_sold_q2;
          ALTER TABLE product DROP COLUMN units_sold_q3;
          ALTER TABLE product DROP COLUMN units_sold_q4;
        `);
      }
    }
  • 运行迁移:
  • npm run build
    npx medusa migrations run

    步骤 8:验证您的更改

    运行迁移后,您的数据库架构将包含新列,并且它们将在您的 `/admin/products` API 响应中可见。通过向端点发送 `GET` 请求来测试这些更改。

    现在,您的 MedusaJS 项目可以跟踪和显示季度销售数据,使您能够创建满足您需求的功能齐全的销售概览页面。

    构建销售页面 UI

    现在数据已在您的管理仪表板和 Medusa 后端之间无缝流动,是时候为您的管理 UI 实现代码了。本节将指导您构建 **销售概览页面** 的用户界面。

    MedusaJS 与 **Medusa UI** 捆绑在一起,这是一个基于 React 的设计系统,其中包括组件、钩子、实用函数、图标和预制的 Tailwind CSS 类。这些工具可确保您的管理仪表板具有一致且专业的设计。

    您的销售概览页面将包含三个主要部分:

  • 标题:显示收入和客户数量等关键指标。
  • 条形图:按季度显示销售数据。
  • 最近订单部分:列出最近的客户订单。
  • Admin Dashboard

    创建销售页面布局

    在 `page.tsx` 文件中,构建销售页面的布局:

    import { RouteConfig } from '@medusajs/admin';
        import { CircleStack } from '@medusajs/icons';
        import TopCards from './components/TopCards';
        import BarChart from './components/BarChart';
        import RecentOrders from './components/RecentOrders';
        const Sales = () => {
        return (
        
    ); }; // Adding route into the admin dashboard sidebar export const config: RouteConfig = { link: { label: 'Sales', icon: CircleStack, }, }; ; export default Sales;

    接下来,在 `src/admin/routes/sales` 下创建一个组件文件夹;在组件文件夹内,创建以下文件:`TopCards.tsx`、`BarChart.tsx` 和 `RecentOrders.tsx`。

    标题部分

    **Header Section** 显示销售统计数据,例如季度收入和客户总数。在 `TopCards.tsx` 中创建此组件:

    `src/admin/routes/sales/components/TopCards.tsx`

    import { Text } from '@medusajs/ui';
    const TopCards = () => {
      return (
        

    Sales

    See the number of product sold and the number remaining in stock

    $7,845

    Quarterly Revenue

    +18%

    $1,14,783

    YTD Revenue

    +11%

    10,845

    Customers

    +17%

    ); }; export default TopCards;

    条形图部分

    **条形图**按季度可视化销售收入。在“BarChart.tsx”中创建此组件:

    `src/admin/routes/sales/components/BarChart.tsx`

    import React, { useState, useEffect } from 'react';
    import { Bar } from 'react-chartjs-2';
    import {
      Chart as ChartJS,
      CategoryScale,
      LinearScale,
      BarElement,
      Title,
      Tooltip,
      Legend,
    } from 'chart.js';
    ChartJS.register(
      CategoryScale,
      LinearScale,
      BarElement,
      Title,
      Tooltip,
      Legend
    );
    
    import {useAdminProducts} from 'medusa-react'
    
    const BarChart = () => {
    
        const { products} = useAdminProducts();
        const [totalRevenueQ1, setTotalRevenueQ1] = useState(null);
        const [totalRevenueQ2, setTotalRevenueQ2] = useState(null);
        const [totalRevenueQ3, setTotalRevenueQ3] = useState(null);
        const [totalRevenueQ4, setTotalRevenueQ4] = useState(null);
    
        useEffect(() => {
          async function calculateTotalRevenue() {
            // Check if products is available
            if (!products) {
              console.error('Products data is not available.');
              return;
            }
    
            // Step 1: Map through Products and calculate revenue for each product, each quarter
            const revenuePerProductQ1 = products.map((product) => {
              const amount = product.variants[0].prices[0].amount;
              const sold_q1 = product.units_sold_q1;
              console.log('amount:', amount, 'sold_q1:', sold_q1);
              return amount * sold_q1;
            });
            const revenuePerProductQ2 = products.map((product) => {
              const amount = product.variants[0].prices[0].amount;
              const sold_q2 = product.units_sold_q2;
              return amount * sold_q2;
            });
            const revenuePerProductQ3 = products.map((product) => {
              const amount = product.variants[0].prices[0].amount;
              const sold_q3 = product.units_sold_q3;
              return amount * sold_q3;
            });
            const revenuePerProductQ4 = products.map((product) => {
              const amount = product.variants[0].prices[0].amount;
              const sold_q4 = product.units_sold_q4;
              return amount * sold_q4;
            });
    
            // Step 2: Sum the individual multiplication results to get the total revenue for each quarter
            const totalRevenueQ1 = revenuePerProductQ1.reduce(
              (total, revenue) => total + revenue,
              0
            );
            const totalRevenueQ2 = revenuePerProductQ2.reduce(
              (total, revenue) => total + revenue,
              0
            );
            const totalRevenueQ3 = revenuePerProductQ3.reduce(
              (total, revenue) => total + revenue,
              0
            );
            const totalRevenueQ4 = revenuePerProductQ4.reduce(
              (total, revenue) => total + revenue,
              0
            );
    
            // Set quarterly totalRevenue states with the calculated value
            setTotalRevenueQ1(totalRevenueQ1);
            setTotalRevenueQ2(totalRevenueQ2);
            setTotalRevenueQ3(totalRevenueQ3);
            setTotalRevenueQ4(totalRevenueQ4);
          }
          // calculate total revenue when products data is available
          if (products) {
            calculateTotalRevenue();
          }
    
        }, [products]);
    
       const data = {
         labels: ['Q1', 'Q2', 'Q3', 'Q4'],
         datasets: [
           {
             label: 'Total Revenue',
             data: [totalRevenueQ1, totalRevenueQ2, totalRevenueQ3, totalRevenueQ4],
             backgroundColor: '#32de84',
             borderColor: 'rgb(0,128,0)',
           },
         ],
       };
    
      return (
        <>
          
    ); }; export default BarChart;
  • useAdminProducts 钩子使您可以访问数据库中可用产品的列表,并且只要您登录到管理仪表板,它就会自动处理身份验证,并在其请求中将凭据传递给 Medusa 后端。您可以在此处了解有关用于管理产品的管理 API 的更多信息。
  • 最近订单部分

    **Recent Orders Section** 列出了客户订单。在 `RecentOrders.tsx` 中创建此组件:

    `src/admin/routes/sales/components/RecentOrders.tsx`

    import { ShoppingBag } from '@medusajs/icons';
    import { useAdminOrders } from 'medusa-react';
    
    const RecentOrders = () => {
      const { orders, isLoading } = useAdminOrders();
    
      return (
        

    Recent Orders

    {isLoading && Loading...} {orders && !orders.length && No Orders} {orders && orders.length > 0 && (
      {orders.map((order, id) => (
    • €{order.payments[0].amount}

      {order.customer.first_name}

      {order.items[0].title}

    • ))}
    )}
    ); }; export default RecentOrders;
  • useAdminOrders 钩子允许您访问客户订单列表,并且只有授权凭证才能访问该列表。
  • useAdminOrders 钩子还允许您访问其所有属性,如 first_name、payments_amount、title 等。
  • 有了这些组件,您的**销售概览页面**现在功能齐全,外观精美。您可以根据需要修改和添加任意数量的功能。

    结论

    本教程演示了如何使用 Next.js、Supabase 和 Tailwind CSS 构建自定义 MedusaJS 管理仪表板。通过遵循这些步骤,您学习了如何设置 Medusa 后端、创建动态用户界面以及扩展 Medusa 功能以构建定制组件(例如销售概览页面)。

    查看 Medusa 文档以了解有关可添加到您的电子商务体验的高级功能和其他自定义选项的更多信息。