Files
juwan-frontend/app/(account)/wallet/page.tsx
T

224 lines
7.7 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 { notifySuccess } from "@/lib/toast"
import { useAuthStore } from "@/store/auth"
import { useWalletStore } from "@/store/wallet"
import {
ArrowDownLeft,
ArrowUpRight,
CreditCard,
DollarSign,
RefreshCw,
Wallet,
} from "lucide-react"
import { 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 = useWalletStore((state) => state.balance)
const transactions = useWalletStore((state) => state.transactions)
const topUp = useWalletStore((state) => state.topUp)
const withdraw = useWalletStore((state) => state.withdraw)
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
const [customAmount, setCustomAmount] = useState("")
const [lastRefreshedAt, setLastRefreshedAt] = useState<string | null>(null)
const filteredTransactions = transactions.filter((tx) => {
if (isConsumer) {
return ["topup", "payment", "refund"].includes(tx.type)
}
return ["income", "withdrawal"].includes(tx.type)
})
const incomeBalance = transactions
.filter((tx) => ["income", "withdrawal"].includes(tx.type))
.reduce((acc, tx) => acc + tx.amount, 0)
const handleTopUp = (rawAmount?: number) => {
const amount = rawAmount ?? Number(customAmount)
if (!Number.isFinite(amount) || amount <= 0) return
topUp(amount)
notifySuccess(`充值成功 +¥${amount}`)
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}`)
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">
¥{isConsumer ? balance.toFixed(2) : incomeBalance.toFixed(2)}
</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"
onClick={() => {
setLastRefreshedAt(new Date().toLocaleTimeString("zh-CN"))
notifySuccess("交易记录已刷新")
}}
>
<RefreshCw className="mr-1 h-3.5 w-3.5" />
</Button>
</CardHeader>
<CardContent className="space-y-3">
{lastRefreshedAt && (
<p className="text-xs text-muted-foreground">{lastRefreshedAt}</p>
)}
{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 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>
)
}