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() {
- + {Icon ? : }

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