feat(wallet): add runtime balance flow and role-gated order posting

This commit is contained in:
zetaloop
2026-02-22 08:17:31 +08:00
parent ea822aaa8d
commit dc629c9472
4 changed files with 207 additions and 26 deletions
+63 -11
View File
@@ -8,13 +8,15 @@ import {
RefreshCw,
Wallet,
} from "lucide-react"
import { useState } from "react"
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 { mockTransactions, walletBalance } from "@/lib/mock"
import { notifySuccess } from "@/lib/toast"
import { useAuthStore } from "@/store/auth"
import { useWalletStore } from "@/store/wallet"
const typeLabels: Record<string, string> = {
topup: "充值",
@@ -35,18 +37,42 @@ const typeIcons: Record<string, typeof ArrowUpRight> = {
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 = mockTransactions.filter((tx) => {
const filteredTransactions = transactions.filter((tx) => {
if (isConsumer) {
return ["topup", "payment", "refund"].includes(tx.type)
}
return ["income", "withdrawal"].includes(tx.type)
})
const incomeBalance = mockTransactions
.filter((tx) => tx.type === "income")
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="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold"></h1>
@@ -59,19 +85,19 @@ export default function WalletPage() {
{isConsumer ? "账户余额" : "收入余额"}
</p>
<p className="text-3xl font-bold mt-1">
¥{isConsumer ? walletBalance.toFixed(2) : incomeBalance.toFixed(2)}
¥{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>
<Button onClick={() => handleTopUp(selectedAmount ?? undefined)}>
<DollarSign className="mr-1 h-4 w-4" />
</Button>
) : (
<Button variant="outline">
<Button variant="outline" onClick={handleWithdraw}>
<CreditCard className="mr-1 h-4 w-4" />
</Button>
@@ -88,15 +114,31 @@ export default function WalletPage() {
<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="outline" className="h-12">
<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" />
<Button></Button>
<Input
placeholder="自定义金额"
type="number"
value={customAmount}
onChange={(event) => {
setCustomAmount(event.target.value)
setSelectedAmount(null)
}}
/>
<Button onClick={() => handleTopUp()}></Button>
</div>
</CardContent>
</Card>
@@ -127,12 +169,22 @@ export default function WalletPage() {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<Button variant="ghost" size="sm">
<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]