Files
juwan-frontend/app/(account)/wallet/page.tsx
T
2026-03-01 22:48:10 +08:00

262 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Badge } from "@/components/ui/badge"
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 { 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 {
ArrowDownLeft,
ArrowUpRight,
CreditCard,
DollarSign,
RefreshCw,
Wallet,
} from "lucide-react"
import { useCallback, useEffect, useMemo, useState } from "react"
const typeLabels: Record<string, string> = {
topup: "充值",
payment: "支付",
income: "收入",
withdrawal: "提现",
refund: "退款",
}
const typeIcons: Record<string, typeof ArrowUpRight> = {
topup: ArrowDownLeft,
payment: ArrowUpRight,
income: ArrowDownLeft,
withdrawal: ArrowUpRight,
refund: ArrowDownLeft,
}
export default function WalletPage() {
const { currentRole } = useAuthStore()
const isConsumer = currentRole === "consumer"
const [balance, setBalance] = useState<number | null>(null)
const [transactions, setTransactions] = useState<WalletTransaction[]>([])
const [isLoading, setIsLoading] = useState(false)
const [loadError, setLoadError] = useState<string | null>(null)
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
const [customAmount, setCustomAmount] = useState("")
const [lastRefreshedAt, setLastRefreshedAt] = useState<string | null>(null)
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 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
notifyInfo("充值暂未开放")
setCustomAmount("")
setSelectedAmount(null)
}
const handleWithdraw = () => {
notifyInfo("提现暂未开放")
setCustomAmount("")
}
return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<h1 className="text-2xl font-bold"></h1>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">
{isConsumer ? "账户余额" : "收入余额"}
</p>
<p className="text-3xl font-bold mt-1">
{balance === null ? (
<span className="text-muted-foreground">--</span>
) : (
<>¥{balance.toFixed(2)}</>
)}
</p>
{loadError && <p className="text-xs text-destructive mt-2">{loadError}</p>}
</div>
<Wallet className="h-10 w-10 text-muted-foreground" />
</div>
<div className="flex gap-2 mt-4">
{isConsumer ? (
<Button onClick={() => handleTopUp(selectedAmount ?? undefined)}>
<DollarSign className="mr-1 h-4 w-4" />
</Button>
) : (
<Button variant="outline" onClick={handleWithdraw}>
<CreditCard className="mr-1 h-4 w-4" />
</Button>
)}
</div>
</CardContent>
</Card>
{isConsumer ? (
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-2">
{[50, 100, 200, 500, 1000, 2000].map((amount) => (
<Button
key={`amount-${amount}`}
variant={selectedAmount === amount ? "default" : "outline"}
className="h-12"
onClick={() => {
setSelectedAmount(amount)
setCustomAmount(amount.toString())
}}
>
¥{amount}
</Button>
))}
</div>
<Separator />
<div className="flex gap-2">
<Input
placeholder="自定义金额"
type="number"
value={customAmount}
onChange={(event) => {
setCustomAmount(event.target.value)
setSelectedAmount(null)
}}
/>
<Button onClick={() => handleTopUp()}></Button>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-lg font-bold">¥1,280.00</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-lg font-bold">¥320.00</p>
</div>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="text-lg font-bold">¥5,400.00</p>
</div>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<Button
variant="ghost"
size="sm"
disabled={isLoading}
onClick={async () => {
await loadWalletData({ showToast: true })
setLastRefreshedAt(new Date().toLocaleTimeString("zh-CN"))
}}
>
<RefreshCw className={`mr-1 h-3.5 w-3.5 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</CardHeader>
<CardContent className="space-y-3">
{lastRefreshedAt && (
<p className="text-xs text-muted-foreground">{lastRefreshedAt}</p>
)}
{isLoading ? (
<div className="text-center py-8 text-muted-foreground text-sm">...</div>
) : filteredTransactions.length > 0 ? (
filteredTransactions.map((tx) => {
const Icon = typeIcons[tx.type]
const isIncome = tx.amount > 0
return (
<div key={tx.id} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
{Icon ? <Icon className="h-4 w-4" /> : <ArrowUpRight className="h-4 w-4" />}
</div>
<div>
<p className="text-sm font-medium">{tx.description}</p>
<p className="text-xs text-muted-foreground">
{new Date(tx.createdAt).toLocaleString("zh-CN")}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${isIncome ? "text-green-600" : ""}`}>
{isIncome ? "+" : ""}¥{Math.abs(tx.amount).toFixed(2)}
</p>
<Badge variant="outline" className="text-[10px]">
{typeLabels[tx.type]}
</Badge>
</div>
</div>
)
})
) : (
<div className="text-center py-8 text-muted-foreground text-sm"></div>
)}
</CardContent>
</Card>
</div>
)
}