Files
mini-programs/src/user_pages/wallet/index.tsx
2025-09-26 17:32:34 +08:00

599 lines
17 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.
import React, { useEffect, useState } from "react";
import { View, Text, Input, Button, Image } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro";
import "./index.scss";
import { CommonPopup } from "@/components";
import httpService from "@/services/httpService";
import { withAuth } from "@/components";
// 交易记录类型
interface Transaction {
id: number;
user_id: number;
transaction_type: string;
freeze_action: string | null;
amount: string;
description: string;
create_time: string;
last_modify_time: string;
related_id: number;
}
// 钱包信息类型
interface WalletInfo {
balance: number;
frozen_balance?: number;
total_balance?: number;
total_income?: number;
total_withdraw?: number;
}
export enum TransactionType {
All = "",
Income = "income",
Expense = "expense",
}
export enum TransactionSubType {
All = "",
GameActivity = "game_activity",
Withdrawal = "withdrawal",
Refund = "refund",
Compensation = "compensation",
}
interface TransactionLoadParams {
page: number;
limit: number;
type: TransactionType;
transaction_sub_type: TransactionSubType;
keyword?: string;
date?: string;
}
interface Option<T> {
label: string;
value: T;
}
const income_expense_options: Option<TransactionType>[] = [
{
label: "全部",
value: TransactionType.All,
},
{
label: "支出",
value: TransactionType.Expense,
},
{
label: "收入",
value: TransactionType.Income,
},
];
const transaction_type_options: Option<TransactionSubType>[] = [
{
label: "全部",
value: TransactionSubType.All,
},
{
label: "组织活动",
value: TransactionSubType.GameActivity,
},
{
label: "提现",
value: TransactionSubType.Withdrawal,
},
{
label: "退款",
value: TransactionSubType.Refund,
},
{
label: "企业赔付",
value: TransactionSubType.Compensation,
},
];
const WalletPage: React.FC = () => {
// 钱包信息状态
const [wallet_info, set_wallet_info] = useState<WalletInfo>({
balance: 0,
});
// 提现弹窗状态
const [show_withdraw_popup, set_show_withdraw_popup] = useState(false);
const [withdraw_amount, set_withdraw_amount] = useState("");
const [submitting, set_submitting] = useState(false);
// 交易记录状态
const [transactions, set_transactions] = useState<Transaction[]>([]);
const [loading_transactions, set_loading_transactions] = useState(false);
// 交易记录过滤状态
const [showFilterPopup, setShowFilterPopup] = useState(false);
const [load_transactions_params, set_load_transactions_params] =
useState<TransactionLoadParams>({
page: 1,
limit: 20,
type: TransactionType.All,
transaction_sub_type: TransactionSubType.All,
keyword: "",
date: "",
});
// 页面显示时加载数据
useDidShow(() => {
load_wallet_data();
load_transactions();
});
// 加载钱包数据
const load_wallet_data = async () => {
try {
const response = await httpService.post("/wallet/balance");
const {
balance,
frozen_balance,
total_balance,
total_income,
total_withdraw,
} = response.data;
set_wallet_info({
balance,
frozen_balance,
total_balance,
total_income,
total_withdraw,
});
} catch (error: any) {
console.error("加载钱包数据失败:", error);
let errorMessage = "加载失败,请重试";
if (
error &&
error.response &&
error.response.data &&
error.response.data.message
) {
errorMessage = error.response.data.message;
} else if (error && error.data && error.data.message) {
errorMessage = error.data.message;
}
Taro.showToast({
title: errorMessage,
icon: "error",
duration: 2000,
});
}
};
// 加载交易记录
const load_transactions = async () => {
setShowFilterPopup(false);
set_load_transactions_params({ ...load_transactions_params, page: 1 });
try {
set_loading_transactions(true);
console.log("开始加载交易记录...");
const response = await httpService.post("/wallet/transactions", {
...load_transactions_params,
});
console.log("交易记录响应:", response);
if (response && response.data && response.data.list) {
set_transactions(response.data.list);
console.log("设置交易记录:", response.data.list);
} else {
console.log("响应数据格式异常:", response);
set_transactions([]);
}
} catch (error: any) {
console.error("加载交易记录失败:", error);
set_transactions([]);
let errorMessage = "加载交易记录失败";
if (
error &&
error.response &&
error.response.data &&
error.response.data.message
) {
errorMessage = error.response.data.message;
} else if (error && error.data && error.data.message) {
errorMessage = error.data.message;
}
Taro.showToast({
title: errorMessage,
icon: "error",
duration: 2000,
});
} finally {
console.log("加载交易记录完成设置loading为false");
set_loading_transactions(false);
}
};
// 处理提现
const handle_withdraw = () => {
if (wallet_info.balance <= 0) {
Taro.showToast({
title: "余额不足",
icon: "error",
duration: 2000,
});
return;
}
set_show_withdraw_popup(true);
};
// 提交提现申请
const submit_withdraw = async () => {
if (!withdraw_amount || parseFloat(withdraw_amount) <= 0) {
Taro.showToast({
title: "请输入有效金额",
icon: "error",
duration: 2000,
});
return;
}
if (parseFloat(withdraw_amount) > wallet_info.balance) {
Taro.showToast({
title: "提现金额不能超过余额",
icon: "error",
duration: 2000,
});
return;
}
set_submitting(true);
// 先调用后端接口获取提现参数
const response = await httpService.post("/wallet/withdraw", {
amount: parseFloat(withdraw_amount),
transfer_remark: "用户申请提现",
});
// 根据后端返回的数据结构解析参数
const { mch_id, app_id, package_info, open_id } = response.data;
console.log("/wallet/withdraw:", response.data);
// 调用微信商户转账接口
(Taro as any).requestMerchantTransfer({
mchId: mch_id,
appId: app_id,
package: package_info,
openId: open_id,
success: (res) => {
console.log("微信转账成功:", res);
Taro.showToast({
title: "提现成功",
icon: "success",
duration: 2000,
});
// 关闭弹窗并重置状态
set_show_withdraw_popup(false);
set_withdraw_amount("");
// 重新加载数据
load_wallet_data();
},
fail: (res) => {
console.log("微信转账失败:", res);
},
});
};
// 格式化金额显示
const format_amount = (amount: number | string) => {
const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
return numAmount.toFixed(2);
};
// 格式化时间显示
const format_time = (time: string) => {
const date = new Date(time);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return {
date: `2025-${month}-${day}`,
time: `${hours}:${minutes}:${seconds}`,
};
};
// 获取交易类型文字
const get_transaction_type_text = (transaction: Transaction) => {
// 如果有描述信息,优先使用描述
if (transaction.description) {
return transaction.description;
}
const typeMap: { [key: string]: string } = {
income: "收入",
expense: "支出",
freeze: "冻结",
unfreeze: "解冻",
};
let typeText =
typeMap[transaction.transaction_type] || transaction.transaction_type;
// 如果有冻结操作,添加到类型文字中
if (transaction.freeze_action) {
const freezeMap: { [key: string]: string } = {
freeze: "冻结",
unfreeze: "解冻",
};
typeText += `(${freezeMap[transaction.freeze_action]})`;
}
return typeText;
};
// 获取金额显示(带符号)
const get_amount_display = (transaction: Transaction) => {
const isPositive = transaction.transaction_type === "income";
const prefix = isPositive ? "+" : "-";
return `${prefix}${format_amount(transaction.amount)}`;
};
const show_filter_popup = () => {
setShowFilterPopup(true);
};
return (
<View className="wallet_page">
{/* 钱包主卡片 */}
<View className="wallet_main_card">
{/* 头部信息 */}
<View className="card_header">
<Text className="header_title"></Text>
<Text className="modify_password"></Text>
</View>
{/* 余额显示 */}
<View className="balance_display">
<View className="amount_section">
<View className="amount_container">
<Text className="currency_symbol">¥</Text>
<View className="amount_group">
<Text className="main_amount">
{Math.floor(wallet_info.balance)}
</Text>
<Text className="decimal_amount">
.
{((wallet_info.balance % 1) * 100)
.toFixed(0)
.padStart(2, "0")}
</Text>
</View>
</View>
<Button className="withdraw_btn" onClick={handle_withdraw}>
</Button>
</View>
<Text className="available_amount">
¥{format_amount(wallet_info.balance)}
</Text>
</View>
</View>
{/* 功能按钮区域 */}
<View className="function_buttons">
<View className="function_item" onClick={show_filter_popup}>
<Text className="function_text"></Text>
<Image
className="function_icon"
src={require("@/static/wallet/arrow-down.svg")}
/>
</View>
<View
className="function_item"
onClick={() =>
Taro.navigateTo({ url: "/user_pages/queryTransactions/index" })
}
>
<Image
className="function_icon"
src={require("@/static/wallet/search.svg")}
/>
<Text className="function_text"></Text>
</View>
<View
className="function_item"
onClick={() =>
Taro.navigateTo({ url: "/user_pages/downloadBill/index" })
}
>
<Image
className="function_icon"
src={require("@/static/wallet/download.svg")}
/>
<Text className="function_text"></Text>
</View>
<View className="function_item">
<Image
className="function_icon"
src={require("@/static/wallet/custom-service.svg")}
/>
<Text className="function_text"></Text>
</View>
</View>
{/* 现金明细 */}
<View className="transaction_history">
{/* 标题栏 */}
<View className="history_header">
<Text className="history_title"></Text>
<View className="month_selector">
<Text className="current_month">2025-09</Text>
</View>
</View>
{/* 交易记录列表 */}
<View className="transaction_list">
{loading_transactions ? (
<View className="loading_state">
<Text className="loading_text">...</Text>
</View>
) : transactions.length > 0 ? (
transactions.map((transaction) => {
const timeInfo = format_time(transaction.create_time);
return (
<View
key={transaction.id}
className="transaction_item"
onClick={() => {
Taro.navigateTo({
url: `/user_pages/billDetail/index?id=${transaction.id}`,
});
}}
>
<View className="transaction_left">
<Text className="transaction_title">
{get_transaction_type_text(transaction)}
</Text>
<View className="transaction_time">
<Text className="transaction_date">{timeInfo.date}</Text>
<Text className="transaction_clock">{timeInfo.time}</Text>
</View>
</View>
<View className="transaction_right">
<Text className="transaction_amount">
{get_amount_display(transaction)}
</Text>
<Text className="balance_info">
¥{format_amount(wallet_info.balance)}
</Text>
</View>
</View>
);
})
) : (
<View className="empty_state">
<Text className="empty_text"></Text>
</View>
)}
</View>
</View>
{/* 提现弹窗 */}
<CommonPopup
visible={show_withdraw_popup}
onClose={() => set_show_withdraw_popup(false)}
onConfirm={submit_withdraw}
title="申请提现"
className="withdraw_popup"
>
<View className="popup_content">
<View className="form_section">
{/* 提现金额 */}
<View className="form_item">
<Text className="form_label"></Text>
<View className="input_wrapper">
<Text className="currency_symbol">¥</Text>
<Input
className="amount_input with_symbol"
type="digit"
placeholder="请输入提现金额"
value={withdraw_amount}
onInput={(e) => set_withdraw_amount(e.detail.value)}
/>
</View>
<Text className="balance_tip">
¥{format_amount(wallet_info.balance)}
</Text>
</View>
{/* 提现说明 */}
<View className="form_item">
<Text className="form_label"></Text>
<View className="withdraw_desc">
<Text className="desc_text">
1-3
</Text>
</View>
</View>
</View>
</View>
</CommonPopup>
{/* 筛选账单弹窗 */}
<CommonPopup
visible={showFilterPopup}
onClose={() => setShowFilterPopup(false)}
onConfirm={load_transactions}
title="选择筛选项"
className="filter_popup"
>
<View className="popup_content">
<View className="form_section">
<View className="form_item">
<Text className="form_label"></Text>
<View className="options_wrapper">
{income_expense_options.map(
(option: Option<TransactionType>) => (
<View
className={
load_transactions_params.type === option.value
? "option_item active"
: "option_item"
}
key={option.value}
onClick={() => {
set_load_transactions_params({
...load_transactions_params,
type: option.value,
});
}}
>
{option.label}
</View>
)
)}
</View>
</View>
<View className="form_item">
<Text className="form_label"></Text>
<View className="options_wrapper">
{transaction_type_options.map(
(option: Option<TransactionSubType>) => (
<View
className={
load_transactions_params.transaction_sub_type ===
option.value
? "option_item active"
: "option_item"
}
key={option.value}
onClick={() => {
set_load_transactions_params({
...load_transactions_params,
transaction_sub_type: option.value,
});
}}
>
{option.label}
</View>
)
)}
</View>
</View>
</View>
</View>
</CommonPopup>
</View>
);
};
export default withAuth(WalletPage);