如何使用 NextJS、Supabase 和 Tailwind CSS 构建自定义 MedusaJS 管理仪表板
在管理电子商务业务时,没有两个业务需求是相同的。例如,一家电子商务商店可能需要一个专注于跟踪实时库存的仪表板,而另一家则优先考虑可视化销售趋势和客户参与度。这就是为什么现成的管理仪表板或预建模板经常不尽人意:它们不是为满足个别企业的特定需求而设计的。
然而,像 Medusa 这样的可定制解决方案通过提供构建模块和 REST API 端点来解决这个问题,这些模块和端点是构建可适应业务当前需求的自定义电子商务解决方案所需的。这可确保您能够控制电子商务生态系统的各个方面。
在本文中,你将了解如何:
在本指南结束时,您将拥有一个如下所示的管理仪表板:

先决条件
要学习本教程,您需要满足以下条件:
在本地设置您的 Medusa 商店
请按照以下步骤设置您的 Medusa 商店:
步骤 1:使用 Supabase 创建 PostgreSQL 数据库
第 2 步:获取数据库连接字符串
步骤 3:在本地安装并设置 Medusa
运行以下命令来安装并设置您的 Medusa 项目,并将其连接到您的 Supabase 数据库:
npx create-medusa-app@latest - seed - db-url postgresql://postgres:@ .supabase.co:5432/postgres
替换 ``,并使用您在**步骤 1** 中创建的数据库密码。
**命令标志解释:**
您可以在文档中看到 create-medusa-app 接受的其他 CLI 选项。
安装完成后,您的项目将在以下端口提供服务:
要访问管理仪表板,请创建一个管理员帐户或使用默认凭据(电子邮件:admin@medusa-test.com 密码:“supersecret”)
添加自定义销售概览页面。
除了默认功能外,MedusaJS 还允许您向管理仪表板添加自定义路线,从而让您能够跟踪特定的业务指标,例如销售业绩。以下是添加自定义**销售概览**页面的方法:
1. 创建自定义管理界面路由
MedusaJS 管理路由是 `src/admin/routes` 目录中的 React 组件。要为销售创建自定义路由:
const Sales = () => {
return (
See the number of products sold and the number remaining in stock
)
}
export default Sales访问 http://localhost:7001/a/sales 查看您的自定义页面。但是,无法从侧边栏访问;您将在下一节中修复此问题。
2. 将路线添加到侧边栏
要使路线可从侧边栏访问:
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**销售**路线现在将出现在管理仪表板侧栏上。
扩展产品实体以添加自定义字段
要使**销售概览**页面正常运行,您需要确保您的数据库支持所有必需的数据。具体来说,您需要显示每种产品的**当前库存**、**价格**和**销售单位**。虽然前两个在默认架构中可用,但`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 类。这些工具可确保您的管理仪表板具有一致且专业的设计。
您的销售概览页面将包含三个主要部分:
创建销售页面布局
在 `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;最近订单部分
**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;有了这些组件,您的**销售概览页面**现在功能齐全,外观精美。您可以根据需要修改和添加任意数量的功能。
结论
本教程演示了如何使用 Next.js、Supabase 和 Tailwind CSS 构建自定义 MedusaJS 管理仪表板。通过遵循这些步骤,您学习了如何设置 Medusa 后端、创建动态用户界面以及扩展 Medusa 功能以构建定制组件(例如销售概览页面)。
查看 Medusa 文档以了解有关可添加到您的电子商务体验的高级功能和其他自定义选项的更多信息。