From 83ea3fea97fc61e88f00ae605253f1cf26ba25c2 Mon Sep 17 00:00:00 2001
From: zetaloop
Date: Sun, 1 Mar 2026 22:48:10 +0800
Subject: [PATCH] feat(wallet): migrate to backend API
---
app/(account)/wallet/page.tsx | 94 ++++++++++----
.../dashboard/shop/income/page.tsx | 121 +++++++++++++-----
lib/api/transactions.ts | 59 ++++++++-
3 files changed, 213 insertions(+), 61 deletions(-)
diff --git a/app/(account)/wallet/page.tsx b/app/(account)/wallet/page.tsx
index 2d4fd28..6b3ba66 100644
--- a/app/(account)/wallet/page.tsx
+++ b/app/(account)/wallet/page.tsx
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
-import { notifySuccess } from "@/lib/toast"
+import { requestWithAuth } from "@/lib/api"
+import { getWalletBalance, listWalletTransactions } from "@/lib/api/transactions"
+import { toApiError } from "@/lib/errors"
+import { notifyInfo, notifySuccess } from "@/lib/toast"
+import type { WalletTransaction } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
-import { useWalletStore } from "@/store/wallet"
import {
ArrowDownLeft,
ArrowUpRight,
@@ -16,7 +19,7 @@ import {
RefreshCw,
Wallet,
} from "lucide-react"
-import { useState } from "react"
+import { useCallback, useEffect, useMemo, useState } from "react"
const typeLabels: Record = {
topup: "充值",
@@ -37,39 +40,66 @@ const typeIcons: Record = {
export default function WalletPage() {
const { currentRole } = useAuthStore()
const isConsumer = currentRole === "consumer"
- const balance = useWalletStore((state) => state.balance)
- const transactions = useWalletStore((state) => state.transactions)
- const topUp = useWalletStore((state) => state.topUp)
- const withdraw = useWalletStore((state) => state.withdraw)
+
+ const [balance, setBalance] = useState(null)
+ const [transactions, setTransactions] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [loadError, setLoadError] = useState(null)
+
const [selectedAmount, setSelectedAmount] = useState(null)
const [customAmount, setCustomAmount] = useState("")
const [lastRefreshedAt, setLastRefreshedAt] = useState(null)
- const filteredTransactions = transactions.filter((tx) => {
- if (isConsumer) {
- return ["topup", "payment", "refund"].includes(tx.type)
- }
- return ["income", "withdrawal"].includes(tx.type)
- })
+ const filteredTransactions = useMemo(() => {
+ return transactions.filter((tx) => {
+ if (isConsumer) {
+ return ["topup", "payment", "refund"].includes(tx.type)
+ }
+ return ["income", "withdrawal"].includes(tx.type)
+ })
+ }, [isConsumer, transactions])
- const incomeBalance = transactions
- .filter((tx) => ["income", "withdrawal"].includes(tx.type))
- .reduce((acc, tx) => acc + tx.amount, 0)
+ const loadWalletData = useCallback(async (options?: { showToast?: boolean }) => {
+ setIsLoading(true)
+ setLoadError(null)
+
+ try {
+ const res = await requestWithAuth(async () => {
+ const [b, items] = await Promise.all([
+ getWalletBalance(),
+ listWalletTransactions({ offset: 0, limit: 1000 }),
+ ])
+ return { balance: b, transactions: items }
+ })
+ if (!res) {
+ setLoadError("请先登录")
+ return
+ }
+
+ setBalance(res.balance)
+ setTransactions(res.transactions)
+ if (options?.showToast) notifySuccess("交易记录已刷新")
+ } catch (error) {
+ setLoadError(toApiError(error).msg)
+ } finally {
+ setIsLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ void loadWalletData()
+ }, [loadWalletData])
const handleTopUp = (rawAmount?: number) => {
const amount = rawAmount ?? Number(customAmount)
if (!Number.isFinite(amount) || amount <= 0) return
- topUp(amount)
- notifySuccess(`充值成功 +¥${amount}`)
+ notifyInfo("充值暂未开放")
setCustomAmount("")
setSelectedAmount(null)
}
const handleWithdraw = () => {
- const amount = Number(customAmount || "0") || Number(incomeBalance.toFixed(2))
- if (!Number.isFinite(amount) || amount <= 0) return
- withdraw(amount)
- notifySuccess(`提现申请已提交 ¥${amount}`)
+ notifyInfo("提现暂未开放")
setCustomAmount("")
}
@@ -85,8 +115,13 @@ export default function WalletPage() {
{isConsumer ? "账户余额" : "收入余额"}
- ¥{isConsumer ? balance.toFixed(2) : incomeBalance.toFixed(2)}
+ {balance === null ? (
+ --
+ ) : (
+ <>¥{balance.toFixed(2)}>
+ )}
+ {loadError && {loadError}
}
@@ -172,12 +207,13 @@ export default function WalletPage() {
@@ -185,7 +221,9 @@ export default function WalletPage() {
{lastRefreshedAt && (
最近刷新:{lastRefreshedAt}
)}
- {filteredTransactions.length > 0 ? (
+ {isLoading ? (
+ 加载中...
+ ) : filteredTransactions.length > 0 ? (
filteredTransactions.map((tx) => {
const Icon = typeIcons[tx.type]
const isIncome = tx.amount > 0
@@ -193,7 +231,7 @@ export default function WalletPage() {
{tx.description}
diff --git a/app/(dashboard)/dashboard/shop/income/page.tsx b/app/(dashboard)/dashboard/shop/income/page.tsx
index 2f33e07..8b04a92 100644
--- a/app/(dashboard)/dashboard/shop/income/page.tsx
+++ b/app/(dashboard)/dashboard/shop/income/page.tsx
@@ -10,13 +10,17 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
-import { listTransactions } from "@/lib/api"
+import { requestWithAuth } from "@/lib/api"
+import { listWalletTransactions } from "@/lib/api/transactions"
import { isActiveOrder, isCompletedOrder } from "@/lib/domain/order-filters"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
+import { toApiError } from "@/lib/errors"
+import type { WalletTransaction } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react"
+import { useEffect, useState } from "react"
export default function ShopIncomePage() {
const userId = useAuthStore((state) => state.user?.id)
@@ -24,6 +28,41 @@ export default function ShopIncomePage() {
const orders = useOrderStore((state) => state.orders)
const shop = resolveOwnerShop(userId, shops)
+ const [transactions, setTransactions] = useState
([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [loadError, setLoadError] = useState(null)
+
+ useEffect(() => {
+ if (!shop) return
+
+ let cancelled = false
+
+ async function load() {
+ setIsLoading(true)
+ setLoadError(null)
+ try {
+ const res = await requestWithAuth(() => listWalletTransactions({ offset: 0, limit: 1000 }))
+ if (cancelled) return
+ if (!res) {
+ setLoadError("请先登录")
+ setTransactions([])
+ return
+ }
+ setTransactions(res)
+ } catch (error) {
+ if (cancelled) return
+ setLoadError(toApiError(error).msg)
+ } finally {
+ if (!cancelled) setIsLoading(false)
+ }
+ }
+
+ void load()
+ return () => {
+ cancelled = true
+ }
+ }, [shop])
+
if (!shop) {
return 当前账号没有可管理的店铺
}
@@ -45,7 +84,7 @@ export default function ShopIncomePage() {
.reduce((acc, order) => acc + order.totalPrice, 0)
const shopOrderIds = new Set(shopOrders.map((order) => order.id))
- const relatedTransactions = listTransactions().filter((transaction) => {
+ const relatedTransactions = transactions.filter((transaction) => {
if (transaction.type === "withdrawal") return true
if (transaction.type !== "income") return false
const match = transaction.description.match(/ord[-\d]+/)
@@ -102,36 +141,58 @@ export default function ShopIncomePage() {
- {relatedTransactions.map((transaction) => (
-
-
-
- {transaction.amount > 0 ? (
-
- ) : (
-
- )}
-
0 ? "default" : "secondary"}>
- {transaction.type === "topup"
- ? "充值"
- : transaction.type === "payment"
- ? "支付"
- : transaction.type === "income"
- ? "收入"
- : transaction.type === "withdrawal"
- ? "提现"
- : "退款"}
-
-
+ {isLoading ? (
+
+
+ 加载中...
- {transaction.description}
- 0 ? "text-green-600" : "text-red-600"}>
- {transaction.amount > 0 ? "+" : ""}
- {transaction.amount}
-
- {new Date(transaction.createdAt).toLocaleString()}
- ))}
+ ) : loadError ? (
+
+
+ {loadError}
+
+
+ ) : relatedTransactions.length > 0 ? (
+ relatedTransactions.map((transaction) => (
+
+
+
+ {transaction.amount > 0 ? (
+
+ ) : (
+
+ )}
+
0 ? "default" : "secondary"}>
+ {transaction.type === "topup"
+ ? "充值"
+ : transaction.type === "payment"
+ ? "支付"
+ : transaction.type === "income"
+ ? "收入"
+ : transaction.type === "withdrawal"
+ ? "提现"
+ : "退款"}
+
+
+
+ {transaction.description}
+ 0 ? "text-green-600" : "text-red-600"}
+ >
+ {transaction.amount > 0 ? "+" : ""}
+ {transaction.amount}
+
+ {new Date(transaction.createdAt).toLocaleString()}
+
+ ))
+ ) : (
+
+
+ 暂无交易记录
+
+
+ )}
diff --git a/lib/api/transactions.ts b/lib/api/transactions.ts
index 7ced12f..7dd5b0d 100644
--- a/lib/api/transactions.ts
+++ b/lib/api/transactions.ts
@@ -1,5 +1,58 @@
-import { useWalletStore } from "@/store/wallet"
+import type { WalletTransaction } from "@/lib/types"
-export function listTransactions() {
- return useWalletStore.getState().transactions
+import { httpJson } from "./http"
+
+type Paginated = {
+ items: T[]
+ meta: {
+ total: number
+ offset: number
+ limit: number
+ }
+}
+
+export type ListWalletTransactionsOptions = {
+ offset?: number
+ limit?: number
+}
+
+function withOffsetLimit(path: string, options?: ListWalletTransactionsOptions): string {
+ const offset = options?.offset ?? 0
+ const limit = options?.limit ?? 1000
+ const searchParams = new URLSearchParams({
+ offset: String(offset),
+ limit: String(limit),
+ })
+ return `${path}?${searchParams.toString()}`
+}
+
+function unwrapWalletBalance(value: unknown): number | undefined {
+ if (typeof value === "number") return value
+ if (typeof value !== "object" || value === null) return undefined
+
+ const v = value as { balance?: unknown; amount?: unknown }
+ if (typeof v.balance === "number") return v.balance
+ if (typeof v.amount === "number") return v.amount
+ return undefined
+}
+
+export async function getWalletBalance(): Promise {
+ const res = await httpJson("/api/v1/wallet/balance", { cache: "no-store" })
+ const balance = unwrapWalletBalance(res)
+ if (balance === undefined) {
+ throw new Error("Invalid wallet balance response")
+ }
+ return balance
+}
+
+export async function listWalletTransactions(
+ options?: ListWalletTransactionsOptions,
+): Promise {
+ const res = await httpJson>(
+ withOffsetLimit("/api/v1/wallet/transactions", options),
+ {
+ cache: "no-store",
+ },
+ )
+ return res.items
}