first commit
This commit is contained in:
139
frontend/src/pages/dashboard/Wallet.tsx
Normal file
139
frontend/src/pages/dashboard/Wallet.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { walletService, TransactionInfo } from "../../services/walletService";
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
topup: "充值",
|
||||
consume: "消费",
|
||||
refund: "退款",
|
||||
};
|
||||
|
||||
export default function Wallet() {
|
||||
const [balance, setBalance] = useState("0");
|
||||
const [code, setCode] = useState("");
|
||||
const [transactions, setTransactions] = useState<TransactionInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [b, txns] = await Promise.all([
|
||||
walletService.getBalance(),
|
||||
walletService.transactions(),
|
||||
]);
|
||||
setBalance(b.balance);
|
||||
setTransactions(txns);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleRedeem = async () => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
if (!code.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const txn = await walletService.redeem(code.trim());
|
||||
setSuccess(`充值成功!+¥${txn.amount}`);
|
||||
setCode("");
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">钱包</h2>
|
||||
|
||||
{/* Balance */}
|
||||
<div className="bg-superdream-panel rounded-xl p-6 mb-4">
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">当前余额</p>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">¥{balance}</p>
|
||||
</div>
|
||||
|
||||
{/* Redeem */}
|
||||
<div className="bg-superdream-panel rounded-xl p-4 mb-4">
|
||||
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-2">兑换码充值</label>
|
||||
|
||||
{error && (
|
||||
<div className="mb-3 p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-3 p-3 rounded-lg bg-green-500/10 border border-green-500/30 text-green-400 text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="输入兑换码"
|
||||
className="flex-1 px-3 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white text-sm focus:border-superdream-accent focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRedeem}
|
||||
disabled={loading || !code.trim()}
|
||||
className="px-4 py-2 rounded-lg bg-superdream-primary text-gray-900 dark:text-white text-sm font-medium hover:bg-purple-600 transition disabled:opacity-50"
|
||||
>
|
||||
{loading ? "兑换中..." : "兑换"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transactions */}
|
||||
<div className="bg-superdream-panel rounded-xl overflow-hidden">
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 px-4 py-3 border-b border-gray-200 dark:border-gray-800">
|
||||
交易记录
|
||||
</h3>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-gray-600 dark:text-gray-400 border-b border-gray-200 dark:border-gray-800">
|
||||
<th className="text-left px-4 py-3 font-medium">类型</th>
|
||||
<th className="text-left px-4 py-3 font-medium">金额</th>
|
||||
<th className="text-left px-4 py-3 font-medium">余额</th>
|
||||
<th className="text-left px-4 py-3 font-medium">备注</th>
|
||||
<th className="text-left px-4 py-3 font-medium">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-gray-500">
|
||||
暂无交易记录
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
transactions.map((txn) => (
|
||||
<tr key={txn.id} className="border-b border-gray-200/50 dark:border-gray-800/50 hover:bg-gray-200/30 dark:hover:bg-gray-800/30">
|
||||
<td className="px-4 py-3 text-gray-900 dark:text-white">
|
||||
{TYPE_LABELS[txn.type] || txn.type}
|
||||
</td>
|
||||
<td className={`px-4 py-3 font-medium ${txn.type === "consume" ? "text-red-400" : "text-green-400"}`}>
|
||||
{txn.type === "consume" ? "-" : "+"}¥{txn.amount}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300">¥{txn.balance_after}</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">{txn.reference_id || "-"}</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">
|
||||
{new Date(txn.created_at).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user