Files
mini-programs/src/user_pages/wallet/index.tsx
张成 1226350099 1
2025-11-14 23:14:18 +08:00

778 lines
23 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, useReachBottom } from "@tarojs/taro";
import "./index.scss";
import { CommonPopup, EmptyState } from "@/components";
import httpService from "@/services/httpService";
import { withAuth, GeneralNavbar } from "@/components";
import { PopupPicker } from "@/components/Picker/index";
import { handleCustomerService } from "@/services/userService";
import img from "@/config/images";
// 交易记录类型
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;
type_text: string | null;
total_balance_after: string;
}
// 钱包信息类型
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 currentPage = Taro.getCurrentInstance();
const pageConfig = currentPage.page?.config;
const pageTitle = pageConfig?.navigationBarTitleText;
useReachBottom(() => {
if (load_transactions_params.page >= totalPages) return;
// 加载更多方法
set_load_transactions_params((prev) => {
return {
...prev,
page: prev.page + 1,
};
});
});
// 钱包信息状态
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 [password_status, set_password_status] = useState(false);
// 交易记录状态
const [transactions, set_transactions] = useState<Transaction[]>([]);
const [loading_transactions, set_loading_transactions] = useState(false);
// 交易记录过滤状态
const [showFilterPopup, setShowFilterPopup] = useState(false);
const [showMonthPicker, setShowMonthPicker] = useState(false);
const [filterParams, setFilterParams] = useState({
type: TransactionType.All,
transaction_sub_type: TransactionSubType.All,
});
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const [load_transactions_params, set_load_transactions_params] =
useState<TransactionLoadParams>({
page: 1,
limit: 20,
type: TransactionType.All,
transaction_sub_type: TransactionSubType.All,
keyword: "",
date: `${year}-${month}`,
});
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
load_transactions();
}, [load_transactions_params]);
// 页面显示时加载数据
useDidShow(() => {
const currentPage = Taro.getCurrentInstance().page;
const updateList = currentPage.data.updateList;
if (updateList) {
set_transactions([]);
// 直接使用新参数调用加载方法
const newParams = {
...load_transactions_params,
page: 1,
};
set_load_transactions_params(newParams);
load_transactions(); // 立即调用
// 清除标记
currentPage.setData({ updateList: null });
}
load_wallet_data();
check_password_status();
});
const modify_load_transactions_params = () => {
set_transactions([]);
const { type, transaction_sub_type } = filterParams;
set_load_transactions_params((prev) => {
return {
...prev,
page: 1,
type,
transaction_sub_type,
};
});
};
const check_password_status = async () => {
try {
const res = await httpService.post("/wallet/check_password_status");
set_password_status(res.data.is_password_set);
} catch (e) {
console.error("检查交易密码状态失败:", e);
}
};
// 加载钱包数据
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 {
if (!transactions.length) 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([...transactions, ...response.data.list]);
setTotalPages(response.data.totalPages);
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 navigateToSetTransactionPassword = (type: "set" | "reset") => {
let url = "";
if (type === "set") {
url = `/user_pages/setTransactionPassword/index?type=${type}`;
} else if (type === "reset") {
url = `/user_pages/validPhone/index`;
}
Taro.navigateTo({
url,
});
};
// 处理提现
const handle_withdraw = () => {
if (!password_status) {
navigateToSetTransactionPassword("set");
return;
}
if (wallet_info.balance <= 0) {
Taro.showToast({
title: "可提现金额不足",
icon: "error",
duration: 2000,
});
return;
}
Taro.navigateTo({
url: "/user_pages/withdrawal/index",
});
// 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) => {
time = time.replace(/-/g, "/");
const date = new Date(time);
const year = String(date.getFullYear());
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: `${year}-${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);
};
const handleFilterCancel = () => {
setShowFilterPopup(false);
setFilterParams({
type: load_transactions_params.type,
transaction_sub_type: load_transactions_params.transaction_sub_type,
});
};
return (
<View className="wallet_page">
{/* 导航栏 */}
{/* <View className="custom-navbar">
<View className="detail-navigator">
<View
className="detail-navigator-back"
onClick={() => {
Taro.navigateBack();
}}
>
<Image
className="detail-navigator-back-icon"
src={img.ICON_NAVIGATOR_BACK}
/>
<Text>{pageTitle}</Text>
</View>
</View>
</View> */}
{/* 顶部导航栏 */}
<GeneralNavbar
title={pageTitle}
showBack={true}
showAvatar={false}
onBack={() => {
Taro.navigateBack();
}}
/>
{/* 钱包主卡片 */}
<View className="wallet_main_card">
{/* 头部信息 */}
<View className="card_header">
<Text className="header_title"></Text>
<Text
className="modify_password"
onClick={() => navigateToSetTransactionPassword("reset")}
>
</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">
{wallet_info.total_balance?.toFixed(2)}
</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" onClick={handleCustomerService}>
<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"
onClick={() => setShowMonthPicker(true)}
>
<Text className="current_month">
{load_transactions_params.date}
</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">
<View>
<Text
className={`type_text_tag ${
transaction.type_text === "已提现" ? "success" : ""
}`}
>
{transaction.type_text}
</Text>
<Text className="transaction_amount">
{get_amount_display(transaction)}
</Text>
</View>
<Text className="balance_info">
¥{format_amount(transaction.total_balance_after)}
</Text>
</View>
</View>
);
})
) : (
// <View className="empty_state">
// <Text className="empty_text">暂无交易记录</Text>
// </View>
<EmptyState
text="钱包空空如也,组织球局赚点球费吧"
styles={{ padding: "unset" }}
/>
)}
</View>
</View>
{/* 提现弹窗 */}
<CommonPopup
showHeader={true}
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>
{/* 选择月份弹窗 */}
{showMonthPicker && (
<PopupPicker
showHeader={true}
title="选择月份"
visible={showMonthPicker}
setvisible={setShowMonthPicker}
value={[
Number(load_transactions_params.date!.split("-")[0]),
Number(load_transactions_params.date!.split("-")[1]),
]}
type="month"
onChange={(e) => {
set_transactions([]);
const [year, month] = e;
set_load_transactions_params({
...load_transactions_params,
page: 1,
date: `${year}-${String(month).padStart(2, "0")}`,
});
}}
/>
)}
{/* 筛选账单弹窗 */}
<CommonPopup
showHeader={true}
visible={showFilterPopup}
onClose={handleFilterCancel}
onConfirm={modify_load_transactions_params}
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={
filterParams.type === option.value
? "option_item active"
: "option_item"
}
key={option.value}
onClick={() => {
setFilterParams({
...filterParams,
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={
filterParams.transaction_sub_type === option.value
? "option_item active"
: "option_item"
}
key={option.value}
onClick={() => {
setFilterParams({
...filterParams,
transaction_sub_type: option.value,
});
}}
>
{option.label}
</View>
)
)}
</View>
</View>
</View>
</View>
</CommonPopup>
</View>
);
};
export default withAuth(WalletPage);