Medusa.js 除了提供完整的電商 API,也支援 Admin UI 自訂化。這讓我們能直接在後台加上自製的功能頁面,完全符合業務需求。
這篇文章我會示範如何在 Medusa Admin 中建立一個 客製化保健食品需求單管理頁面,並支援查看、編輯、回覆需求。
另外, MedusaJs 內建儀表板是使用 React
去建構的,所以本篇文章都會使用 React
。
預設的 Medusa Admin 提供了商品、訂單、顧客、行銷等模組管理功能。但在實際業務裡,我們常常會有額外的需求,比如我目前需要:
這些需求如果沒有自制儀表板,管理員只能透過 API 操作,效率不高。
👉 有了自制儀表板,就能在 Medusa Admin 介面裡一鍵完成。
在所有自製元件到儀表板,是有可能與本身後端交流的,所以MedusaJS
有一個JS SDK套件
可以簡單地與API
要資料。
初始化sdk
需要以下參數:
MeudsaJS
後端 URL
。true
,預設是 false
。session
。import Medusa from "@medusajs/js-sdk"
export const sdk = new Medusa({
baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
debug: import.meta.env.DEV,
auth: {
type: "session",
},
})
src/widget/
MeudsaJS UI
或者其他到 .tsx 檔API(接口)
src/routes/...
中MeudsaJS UI
或者其他到 .tsx 檔開發 Admin 自製 UI,MedusaJS UI
套件非常好用:
以下是一個完整的頁面程式碼及註解,支援查看、編輯、儲存回覆:
// 匯入 Medusa Admin SDK 的 Route 設定方法
import { defineRouteConfig } from "@medusajs/admin-sdk"
// 匯入圖示
import { TagSolid, Eye } from "@medusajs/icons"
// 匯入 MedusaJS 提供的 UI 元件
import {
Container,
Heading,
createDataTableColumnHelper,
DataTable,
DataTablePaginationState,
useDataTable,
Button,
Drawer,
Label,
Input,
} from "@medusajs/ui"
// 匯入 React Query 與 SDK
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../sdk"
import { useMemo, useState } from "react"
// 資料型別:每筆客製化保健食品需求
type Supplement = {
id: string,
requirement: string, // 使用者需求
sure: boolean, // 是否確認
created_at: string, // 建立時間
response: object // 回覆內容(JSON 格式,含理由 & 成分)
}
// 後端回傳的資料結構
type SupplementsResponse = {
supplements: Supplement[]
count: number
limit: number
offset: number
}
// 建立 DataTable 欄位助手
const columnHelper = createDataTableColumnHelper<Supplement>()
const SupplementsPage = () => {
// === 分頁設定 ===
const limit = 15 // 每頁顯示 15 筆
const [pagination, setPagination] = useState<DataTablePaginationState>({
pageSize: limit,
pageIndex: 0,
})
// offset 計算 (根據目前第幾頁)
const offset = useMemo(() => pagination.pageIndex * limit, [pagination])
// 使用 React Query 取得資料
const { data, isLoading, refetch } = useQuery<SupplementsResponse>({
queryFn: () =>
sdk.client.fetch(`/admin/supplement/showSupplement`, {
query: { limit, offset }, // 傳入分頁參數
}),
queryKey: [["supplement", limit, offset]], // cache key
})
// === Drawer 狀態控制 ===
const [open, setOpen] = useState(false) // Drawer 是否開啟
const [selected, setSelected] = useState<Supplement | null>(null) // 當前選中的需求單
// === 分開控制「理由」與「成分」輸入框 ===
const [reason, setReason] = useState("")
const [formula, setFormula] = useState("")
// === 儲存回覆(理由 + 成分) ===
const handleSave = async () => {
if (!selected) return
try {
await sdk.client.fetch(`/admin/supplement/createResponse`, {
method: "POST",
body: {
id: selected.id,
// 組合成 JSON 格式儲存
response: {
理由: reason,
成分: formula,
},
},
})
// 成功後關閉 Drawer,清空輸入框,重新抓資料
setOpen(false)
setReason("")
setFormula("")
refetch()
} catch (err) {
console.error("更新失敗", err)
}
}
// === 定義 DataTable 欄位 ===
const columns = [
columnHelper.accessor("id", { header: "ID" }),
columnHelper.accessor("requirement", { header: "Requirement" }),
columnHelper.accessor("created_at", { header: "Created At" }),
columnHelper.accessor("sure", {
header: "Sure",
// true/false 轉換成 Yes/No
cell: (info) => (info.getValue() ? "Yes" : "No"),
}),
// 自訂「操作」欄位
columnHelper.display({
id: "actions",
header: "Actions",
cell: ({ row }) => (
<Button
variant="secondary"
size="small"
onClick={() => {
// 設定選中的 row
setSelected(row.original)
// 如果已有回覆資料,帶入「理由」與「成分」到輸入框
if (row.original.response) {
const res = row.original.response as any
setReason(res?.理由 ?? "")
setFormula(res?.成分 ?? "")
} else {
setReason("")
setFormula("")
}
// 打開 Drawer
setOpen(true)
}}
>
<Eye className="w-4 h-4" />
查看
</Button>
),
}),
]
// === 初始化 DataTable ===
const table = useDataTable({
columns,
data: data?.supplements || [],
getRowId: (row) => row.id,
rowCount: data?.count || 0,
isLoading,
pagination: { state: pagination, onPaginationChange: setPagination },
})
return (
<Container className="divide-y p-0">
{/* 主表格 */}
<DataTable instance={table}>
<DataTable.Toolbar>
<Heading>客製化保健食品需求單</Heading>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>
{/* Drawer:顯示選取需求單的詳細資訊 */}
<Drawer open={open} onOpenChange={setOpen}>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>需求單詳情</Drawer.Title>
<Drawer.Description>
查看或編輯 Supplement 資料
</Drawer.Description>
</Drawer.Header>
<Drawer.Body className="flex flex-col gap-4">
{/* 基本資訊 (唯讀) */}
<div className="flex flex-col gap-2">
<Label>ID</Label>
<Input readOnly value={selected?.id ?? ""} />
</div>
<div className="flex flex-col gap-2">
<Label>需求</Label>
<Input readOnly value={selected?.requirement ?? ""} />
</div>
{/* 編輯區:理由 */}
<div className="flex flex-col gap-2">
<Label>理由</Label>
<Input
placeholder="輸入理由..."
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</div>
{/* 編輯區:成分 */}
<div className="flex flex-col gap-2">
<Label>成分</Label>
<Input
placeholder="輸入成分..."
value={formula}
onChange={(e) => setFormula(e.target.value)}
/>
</div>
</Drawer.Body>
{/* Footer:操作按鈕 */}
<Drawer.Footer>
<Button variant="secondary" onClick={() => setOpen(false)}>
關閉
</Button>
<Button variant="primary" onClick={handleSave}>
儲存變更
</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer>
</Container>
)
}
// 註冊這個頁面在 Medusa Admin UI 的路由設定
export const config = defineRouteConfig({
label: "Supplements", // 側邊欄顯示名稱
icon: TagSolid, // 側邊欄的 icon
})
export default SupplementsPage
透過 Medusa Admin SDK
與 @medusajs/ui
,我們可以輕鬆打造符合需求的自製頁面:
DataTable
列出資料Drawer
顯示詳細內容Textarea
編輯並送出回覆這樣一來,業務就能直接在 Medusa Admin 介面中處理需求單,完全不需要跳到 Postman 或其他 API 工具。