添加钱包测试功能

This commit is contained in:
张成
2025-09-21 13:26:38 +08:00
parent 29bc03f242
commit 535b647165
14 changed files with 912 additions and 5 deletions

View File

@@ -30,7 +30,7 @@ const MyselfPage: React.FC = () => {
participated: 0,
},
bio: "加载中...",
location: "加载中...",
city: "加载中...",
occupation: "加载中...",
ntrp_level: "NTRP 3.0",
personal_profile: "加载中...",
@@ -143,10 +143,10 @@ const MyselfPage: React.FC = () => {
});
};
// 处理收藏
const handle_favorites = () => {
// 处理钱包
const handle_wallet = () => {
Taro.navigateTo({
url: "/other_pages/favorites/index",
url: "/user_pages/wallet/index",
});
};
@@ -180,7 +180,7 @@ const MyselfPage: React.FC = () => {
<Text className="action_text"></Text>
</View>
<View className="action_divider"></View>
<View className="action_content" onClick={handle_favorites}>
<View className="action_content" onClick={handle_wallet}>
<Image
className="action_icon"
src={require("@/static/userInfo/wallet.svg")}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: "我的钱包",
});

View File

@@ -0,0 +1,344 @@
// @use '../../scss/common.scss' as *;
.wallet_page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 5px;
.wallet_main_card {
background: white;
border-radius: 20px;
padding: 12px 20px 32px;
margin-bottom: 8px;
.card_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
.header_title {
font-size: 12px;
font-weight: 400;
color: #000;
line-height: 1.5;
}
.modify_password {
font-size: 12px;
font-weight: 400;
color: #007AFF;
line-height: 1.5;
}
}
.balance_display {
.amount_section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.amount_container {
display: flex;
align-items: flex-end;
gap: 8px;
.currency_symbol {
font-family: 'DingTalk JinBuTi', sans-serif;
font-size: 20px;
font-weight: 400;
color: #000;
line-height: 0.8;
}
.amount_group {
display: flex;
align-items: flex-end;
.main_amount {
font-family: 'DingTalk JinBuTi', sans-serif;
font-size: 32px;
font-weight: 400;
color: #000;
line-height: 0.75;
}
.decimal_amount {
font-family: 'DingTalk JinBuTi', sans-serif;
font-size: 20px;
font-weight: 400;
color: #000;
line-height: 0.8;
}
}
}
.withdraw_btn {
background: #000;
color: white;
border: none;
border-radius: 20px;
padding: 8px 20px;
font-size: 14px;
font-weight: 400;
line-height: 1.29;
width: 68px;
height: 34px;
&:active {
background: #333;
}
}
}
.available_amount {
font-size: 12px;
font-weight: 400;
color: #000;
line-height: 1.5;
text-align: left;
}
}
}
.function_buttons {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.function_item {
background: white;
border: 0.5px solid #EBEBEB;
border-radius: 20px;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.05);
height: 24px;
flex: 1;
.function_icon {
width: 12px;
height: 12px;
display: flex;
align-items: center;
justify-content: center;
.icon_text {
font-size: 8px;
}
}
.function_text {
font-size: 12px;
font-weight: 600;
color: #000;
line-height: 1.33;
white-space: nowrap;
}
&:active {
background: #f8f8f8;
}
}
}
.transaction_history {
background: white;
border: 0.5px solid #EBEBEB;
border-radius: 20px;
box-shadow: 0px 0px 36px 0px rgba(0, 0, 0, 0.1);
overflow: hidden;
.history_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 0.5px solid rgba(120, 120, 128, 0.12);
.history_title {
font-size: 16px;
font-weight: 600;
color: #000;
line-height: 1.5;
}
.month_selector {
display: flex;
align-items: center;
gap: 4px;
.current_month {
font-size: 12px;
font-weight: 600;
color: #000;
line-height: 1.5;
}
.dropdown_arrow {
font-size: 10px;
color: #000;
}
}
}
.transaction_list {
.loading_state,
.empty_state {
padding: 40px 20px;
text-align: center;
.loading_text,
.empty_text {
font-size: 14px;
color: #999;
}
}
.transaction_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
.transaction_left {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
.transaction_title {
font-size: 12px;
font-weight: 600;
color: #000;
line-height: 1.5;
text-align: left;
}
.transaction_time {
display: flex;
align-items: center;
align-self: stretch;
gap: 4px;
.transaction_date,
.transaction_clock {
font-size: 10px;
font-weight: 400;
color: rgba(60, 60, 67, 0.6);
line-height: 1.2;
text-align: left;
}
}
}
.transaction_right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
width: 68px;
.transaction_amount {
font-size: 12px;
font-weight: 600;
color: #000;
line-height: 1.5;
text-align: right;
}
.balance_info {
font-size: 10px;
font-weight: 400;
color: rgba(60, 60, 67, 0.6);
line-height: 1.2;
text-align: right;
}
}
}
}
}
}
// 提现弹窗样式
.withdraw_popup {
.popup_content {
padding: 12px;
overflow: hidden;
.form_section {
.form_item {
margin-bottom: 12px;
.form_label {
font-size: 12px;
color: #333;
margin-bottom: 6px;
font-weight: 500;
}
.input_wrapper {
position: relative;
.amount_input {
box-sizing: border-box;
width: 100%;
height: 36px;
border: 1px solid #e8e8e8;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
color: #333;
&:focus {
border-color: #667eea;
box-shadow: none;
}
&::placeholder {
color: #bbb;
}
}
.currency_symbol {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: #999;
pointer-events: none;
}
.amount_input.with_symbol {
padding-left: 28px;
}
}
.balance_tip {
font-size: 10px;
color: #999;
margin-top: 4px;
}
.withdraw_desc {
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
.desc_text {
font-size: 10px;
color: #666;
line-height: 1.3;
}
}
}
}
}
}

View File

@@ -0,0 +1,406 @@
import React, { useState } from "react";
import { View, Text, Input, Button } 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;
}
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);
// 页面显示时加载数据
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 () => {
try {
set_loading_transactions(true);
console.log("开始加载交易记录...");
const response = await httpService.post("/wallet/transactions", {
page: 1,
limit: 20
});
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: package_info, open_id } = 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)}`;
};
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">
<View className="function_icon">
<Text className="icon_text">📄</Text>
</View>
<Text className="function_text"></Text>
</View>
<View className="function_item">
<View className="function_icon">
<Text className="icon_text">🔍</Text>
</View>
<Text className="function_text"></Text>
</View>
<View className="function_item">
<View className="function_icon">
<Text className="icon_text"></Text>
</View>
<Text className="function_text"></Text>
</View>
<View className="function_item">
<View className="function_icon">
<Text className="icon_text">💬</Text>
</View>
<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>
<Text className="dropdown_arrow"></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">
<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>
</View>
);
};
export default withAuth(WalletPage);