fix(wallet): persist balance actions through backend
This commit is contained in:
@@ -6,7 +6,12 @@ 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 {
|
||||
getWalletBalance,
|
||||
listWalletTransactions,
|
||||
topUpWallet,
|
||||
withdrawWallet,
|
||||
} from "@/lib/api/transactions"
|
||||
import { toApiError } from "@/lib/errors"
|
||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||
import type { WalletTransaction } from "@/lib/types"
|
||||
@@ -44,6 +49,7 @@ export default function WalletPage() {
|
||||
const [balance, setBalance] = useState<number | null>(null)
|
||||
const [transactions, setTransactions] = useState<WalletTransaction[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
|
||||
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
|
||||
@@ -59,6 +65,21 @@ export default function WalletPage() {
|
||||
})
|
||||
}, [isConsumer, transactions])
|
||||
|
||||
const incomeSummary = useMemo(() => {
|
||||
const income = transactions
|
||||
.filter((tx) => tx.type === "income")
|
||||
.reduce((total, tx) => total + Number(tx.amount), 0)
|
||||
const withdrawn = transactions
|
||||
.filter((tx) => tx.type === "withdrawal")
|
||||
.reduce((total, tx) => total + Number(tx.amount), 0)
|
||||
|
||||
return {
|
||||
income,
|
||||
available: balance ?? 0,
|
||||
withdrawn,
|
||||
}
|
||||
}, [balance, transactions])
|
||||
|
||||
const loadWalletData = useCallback(
|
||||
async (options?: { showToast?: boolean; silent?: boolean }) => {
|
||||
if (!options?.silent) {
|
||||
@@ -122,17 +143,53 @@ export default function WalletPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTopUp = (rawAmount?: number) => {
|
||||
const handleTopUp = async (rawAmount?: number) => {
|
||||
const amount = rawAmount ?? Number(customAmount)
|
||||
if (!Number.isFinite(amount) || amount <= 0) return
|
||||
notifyInfo("充值暂未开放")
|
||||
setCustomAmount("")
|
||||
setSelectedAmount(null)
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
notifyInfo("请输入有效金额")
|
||||
return
|
||||
}
|
||||
|
||||
setIsMutating(true)
|
||||
try {
|
||||
const res = await requestWithAuth(async () => {
|
||||
await topUpWallet({ amount })
|
||||
await loadWalletData({ silent: true })
|
||||
return true
|
||||
})
|
||||
if (!res) return
|
||||
notifySuccess("充值成功")
|
||||
setCustomAmount("")
|
||||
setSelectedAmount(null)
|
||||
} catch (error) {
|
||||
notifyInfo(toApiError(error).msg)
|
||||
} finally {
|
||||
setIsMutating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleWithdraw = () => {
|
||||
notifyInfo("提现暂未开放")
|
||||
setCustomAmount("")
|
||||
const handleWithdraw = async () => {
|
||||
const amount = Number(customAmount)
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
notifyInfo("请输入有效金额")
|
||||
return
|
||||
}
|
||||
|
||||
setIsMutating(true)
|
||||
try {
|
||||
const res = await requestWithAuth(async () => {
|
||||
await withdrawWallet({ amount })
|
||||
await loadWalletData({ silent: true })
|
||||
return true
|
||||
})
|
||||
if (!res) return
|
||||
notifySuccess("提现成功")
|
||||
setCustomAmount("")
|
||||
} catch (error) {
|
||||
notifyInfo(toApiError(error).msg)
|
||||
} finally {
|
||||
setIsMutating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -159,12 +216,15 @@ export default function WalletPage() {
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
{isConsumer ? (
|
||||
<Button onClick={() => handleTopUp(selectedAmount ?? undefined)}>
|
||||
<Button
|
||||
disabled={isMutating}
|
||||
onClick={() => void handleTopUp(selectedAmount ?? undefined)}
|
||||
>
|
||||
<DollarSign className="mr-1 h-4 w-4" />
|
||||
充值
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={handleWithdraw}>
|
||||
<Button variant="outline" disabled={isMutating} onClick={() => void handleWithdraw()}>
|
||||
<CreditCard className="mr-1 h-4 w-4" />
|
||||
提现
|
||||
</Button>
|
||||
@@ -205,7 +265,9 @@ export default function WalletPage() {
|
||||
setSelectedAmount(null)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => handleTopUp()}>充值</Button>
|
||||
<Button disabled={isMutating} onClick={() => void handleTopUp()}>
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -217,18 +279,30 @@ export default function WalletPage() {
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground">累计收入</p>
|
||||
<p className="text-lg font-bold">¥{incomeSummary.income.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">待结算</p>
|
||||
<p className="text-lg font-bold">¥320.00</p>
|
||||
<p className="text-xs text-muted-foreground">可提现</p>
|
||||
<p className="text-lg font-bold">¥{incomeSummary.available.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">已提现</p>
|
||||
<p className="text-lg font-bold">¥5,400.00</p>
|
||||
<p className="text-lg font-bold">¥{incomeSummary.withdrawn.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="提现金额"
|
||||
type="number"
|
||||
value={customAmount}
|
||||
onChange={(event) => setCustomAmount(event.target.value)}
|
||||
/>
|
||||
<Button variant="outline" disabled={isMutating} onClick={() => void handleWithdraw()}>
|
||||
提现
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -258,7 +332,7 @@ export default function WalletPage() {
|
||||
) : filteredTransactions.length > 0 ? (
|
||||
filteredTransactions.map((tx) => {
|
||||
const Icon = typeIcons[tx.type]
|
||||
const isIncome = Number(tx.amount) > 0
|
||||
const isIncome = tx.type === "topup" || tx.type === "income" || tx.type === "refund"
|
||||
return (
|
||||
<div key={tx.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user