合并代码

This commit is contained in:
筱野
2025-09-28 23:00:57 +08:00
23 changed files with 1901 additions and 422 deletions

View File

@@ -30,6 +30,9 @@ export default defineAppConfig({
"downloadBill/index", // 下载账单 "downloadBill/index", // 下载账单
"downloadBillRecords/index", // 下载账单记录 "downloadBillRecords/index", // 下载账单记录
"billDetail/index", // 账单详情 "billDetail/index", // 账单详情
"setTransactionPassword/index", // 设置交易密码
"validPhone/index", // 验证手机号
"withdrawal/index", // 提现
], ],
}, },
// { // {

View File

@@ -17,7 +17,7 @@ interface PickerOption {
interface PickerProps { interface PickerProps {
visible: boolean; visible: boolean;
setvisible: (visible: boolean) => void; setvisible: (visible: boolean) => void;
options?: PickerOption[][]; options?: PickerOption[][] | PickerOption[];
value?: (string | number)[]; value?: (string | number)[];
type?: "month" | "day" | "hour" | "ntrp" | null; type?: "month" | "day" | "hour" | "ntrp" | null;
img?: string; img?: string;

View File

@@ -29,7 +29,9 @@ export const API_CONFIG = {
CREATE: '/game/create', CREATE: '/game/create',
JOIN: '/game/join', JOIN: '/game/join',
LEAVE: '/game/leave' LEAVE: '/game/leave'
} },
PROFESSIONS: '/professions/tree',
CITIS: '/admin/wch_cities/page'
}; };
// 请求拦截器配置 // 请求拦截器配置

View File

@@ -40,6 +40,17 @@ interface UserDetailData {
}; };
} }
export interface PickerOption {
text: string | number;
value: string | number;
children?: PickerOption[];
}
export interface Profession {
name: string;
children: Profession[] | [];
}
// 用户详细信息接口(从 loginService 移过来) // 用户详细信息接口(从 loginService 移过来)
export interface UserInfoType { export interface UserInfoType {
id: number id: number
@@ -119,6 +130,21 @@ interface BackendGameData {
}[]; }[];
} }
const formatOptions = (data: Profession[]): PickerOption[] => {
return data.map((item: Profession) => {
const { name: text, children } = item;
const itm: PickerOption = {
text,
value: text,
children: children ? formatOptions(children) : []
}
if (!itm.children!.length) {
delete itm.children
}
return itm
})
}
// 用户服务类 // 用户服务类
export class UserService { export class UserService {
// 数据转换函数将后端数据转换为ListContainer期望的格式 // 数据转换函数将后端数据转换为ListContainer期望的格式
@@ -206,7 +232,7 @@ export class UserService {
date_str = `明天(${weekday})`; date_str = `明天(${weekday})`;
} else if (start_date.getTime() === day_after_tomorrow.getTime()) { } else if (start_date.getTime() === day_after_tomorrow.getTime()) {
date_str = `后天(${weekday})`; date_str = `后天(${weekday})`;
} else if(this.is_date_in_this_week(start_time)) { } else if (this.is_date_in_this_week(start_time)) {
date_str = weekday; date_str = weekday;
} else { } else {
date_str = `${start_time.getFullYear()}-${(start_time.getMonth() + 1).toString().padStart(2, '0')}-${start_time.getDate().toString().padStart(2, '0')}(${weekday})`; date_str = `${start_time.getFullYear()}-${(start_time.getMonth() + 1).toString().padStart(2, '0')}-${start_time.getDate().toString().padStart(2, '0')}(${weekday})`;
@@ -238,7 +264,7 @@ export class UserService {
if (response.code === 0) { if (response.code === 0) {
const userData = response.data; const userData = response.data;
return { return {
id: userData.id || '', id: userData.id || '',
nickname: userData.nickname || '', nickname: userData.nickname || '',
avatar: userData.avatar_url || '', avatar: userData.avatar_url || '',
join_date: userData.subscribe_time ? `${new Date(userData.subscribe_time).getFullYear()}${new Date(userData.subscribe_time).getMonth() + 1}月加入` : '', join_date: userData.subscribe_time ? `${new Date(userData.subscribe_time).getFullYear()}${new Date(userData.subscribe_time).getMonth() + 1}月加入` : '',
@@ -492,6 +518,39 @@ export class UserService {
return ''; return '';
} }
} }
// 获取职业树
static async getProfessions(): Promise<[] | PickerOption[]> {
try {
const response = await httpService.post<any>(API_CONFIG.PROFESSIONS);
const { code, data, message } = response;
if (code === 0) {
return formatOptions(data || []);
} else {
throw new Error(message || '获取职业树失败');
}
} catch (error) {
console.error('获取职业树失败:', error);
return [];
}
}
// 获取城市树
static async getCities(): Promise<[] | PickerOption[]> {
try {
const response = await httpService.post<any>(API_CONFIG.CITIS);
const { code, data, message } = response;
if (code === 0) {
return formatOptions(data || []);
} else {
throw new Error(message || '获取城市树失败');
}
} catch (error) {
console.error('获取职业树失败:', error);
return [];
}
}
} }
// 从 loginService 移过来的用户相关方法 // 从 loginService 移过来的用户相关方法

View File

@@ -1,6 +1,10 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { View, Text, Input, Button, Image } from "@tarojs/components"; import { View, Text } from "@tarojs/components";
import { useRouter } from '@tarojs/taro';
import dayjs from 'dayjs';
import Taro from '@tarojs/taro';
import httpService from "@/services/httpService";
import { TransactionType, TransactionSubType } from "@/user_pages/wallet/index"; import { TransactionType, TransactionSubType } from "@/user_pages/wallet/index";
import "./index.scss"; import "./index.scss";
@@ -10,51 +14,84 @@ enum FreezeActions {
} }
interface BillDetail { interface BillDetail {
id: number; id?: number;
transaction_type: TransactionType; transaction_type?: TransactionType;
transaction_sub_type: TransactionSubType; transaction_sub_type?: TransactionSubType;
freeze_action: FreezeActions; freeze_action?: FreezeActions;
amount: number; amount?: number;
description: string; description?: string;
related_id: number; related_id?: number;
create_time: string; create_time?: string;
order_no: string; order_no?: string;
game_title: string; game_title?: string;
order_amount: number; order_amount?: number;
type_text: string; type_text?: string;
sub_type_text: string; sub_type_text?: string;
amount_yuan: string; amount_yuan?: string;
} }
const BillDetail: React.FC = () => { const BillDetail: React.FC = () => {
const [billDetail, setBillDetail] = useState<BillDetail | null>(null); const router = useRouter();
const { id } = router.params;
const [billDetail, setBillDetail] = useState<BillDetail>({});
const getBillDetail = async () => {
try {
const res = await httpService.post<BillDetail>("/wallet/transaction_detail", { transaction_id: id })
if (res.code === 0) {
setBillDetail(res.data);
}
} catch (error) {
console.log(error);
}
};
const copyText = (text: string | undefined) => {
if (!text) {
return;
}
Taro.setClipboardData({
data: text,
success: () => {
Taro.showToast({
title: '复制成功',
icon: 'none'
});
}
});
};
useEffect(() => {
getBillDetail();
}, [id]);
return ( return (
<View className="bill-detail-page"> <View className="bill-detail-page">
<View className="title-text-box"> <View className="title-text-box">
<View className="title-text"> ()</View> <View className="title-text"> ()</View>
<View className="amount-text"> <View className="amount-text">
<Text>+</Text> <Text>{billDetail.transaction_type === 'expense' ? '-' : '+'}</Text>
<Text>65.00</Text> <Text>{billDetail.amount_yuan}</Text>
</View> </View>
</View> </View>
<View className="detail-wrapper"> <View className="detail-wrapper">
<View className="detail-item"> <View className="detail-item">
<Text></Text> <Text></Text>
<Text>2025-02-16 12:21:54</Text> <Text>{billDetail.create_time && dayjs(billDetail.create_time).format('YYYY-MM-DD HH:mm:ss')}</Text>
</View> </View>
<View className="detail-item"> <View className="detail-item">
<Text></Text> <Text></Text>
<Text></Text> <Text>{billDetail.game_title}</Text>
</View> </View>
<View className="detail-item"> <View className="detail-item">
<Text></Text> <Text></Text>
<Text>¥3890.00</Text> <Text>¥{billDetail.amount}</Text>
</View> </View>
<View className="detail-item"> <View className="detail-item">
<Text></Text> <Text></Text>
<View className="with-btn-box"> <View className="with-btn-box">
<Text>89172371293791273912</Text> <Text>{billDetail.order_no}</Text>
<Text className="btn"></Text> <Text className="btn" onClick={() => copyText(billDetail.order_no)}></Text>
</View> </View>
</View> </View>
</View> </View>

View File

@@ -132,3 +132,39 @@
} }
} }
} }
// 过滤弹窗
.filter_popup {
padding: 20px;
.popup_content {
.form_section {
.form_item {
margin-bottom: 20px;
.form_label {
display: inline-block;
font-family: PingFang SC;
font-weight: 600;
font-style: Semibold;
font-size: 16px;
margin-bottom: 20px;
}
.options_wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
.option_item {
background-color: #0000000D;
text-align: center;
padding: 8px;
border-radius: 4px;
&.active {
background-color: #000000;
color: #fff;
}
}
}
}
}
}
}

View File

@@ -1,20 +1,47 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { View, Text, Button } from "@tarojs/components"; import { View, Text, Button, Input } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro"; import Taro, { useDidShow } from "@tarojs/taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "./index.scss"; import "./index.scss";
import { DialogCalendarCard } from "@/components/index"; import { DialogCalendarCard } from "@/components/index";
// import { CalendarUI } from "@/components"; // import { CalendarUI } from "@/components";
import { CommonPopup } from "@/components";
import httpService from "@/services/httpService";
export enum TransactionSubType {
All = "",
GameActivity = "game_activity",
Withdrawal = "withdrawal",
Refund = "refund",
Compensation = "compensation",
}
export enum TransactionType {
All = "",
Income = "income",
Expense = "expense",
}
interface Option<T> {
label: string;
value: T;
}
interface TransactionLoadParams {
transaction_sub_type: TransactionSubType;
date_range?: string[];
}
const DownloadBill: React.FC = () => { const DownloadBill: React.FC = () => {
const [dateRange, setDateRange] = useState({ start: "", end: "" }); const [dateRange, setDateRange] = useState({ start: "", end: "" });
const [transactionSubType, setTransactionSubType] =
useState<TransactionSubType>(TransactionSubType.All);
const [dateType, setDateType] = useState("week"); const [dateType, setDateType] = useState("week");
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [show_download_popup, set_show_download_popup] = useState(false);
const [isFocus, setIsFocus] = useState(false);
const [password, setPassword] = useState<string[]>(new Array(6).fill(""));
useEffect(() => { useEffect(() => {
culculateDateRange(dateType); culculateDateRange(dateType);
}, []); }, []);
const [showFilterPopup, setShowFilterPopup] = useState(false);
const culculateDateRange = (dateType: string) => { const culculateDateRange = (dateType: string) => {
const today = new Date(); const today = new Date();
const year = today.getFullYear(); const year = today.getFullYear();
@@ -49,17 +76,18 @@ const DownloadBill: React.FC = () => {
} }
switch (range) { switch (range) {
case "week": case "week":
setCurrentTimeValue(new Date());
setDateType("week"); setDateType("week");
culculateDateRange("week"); culculateDateRange("week");
break; break;
case "month": case "month":
setCurrentTimeValue(new Date());
setDateType("month"); setDateType("month");
culculateDateRange("month"); culculateDateRange("month");
break; break;
case "custom": case "custom":
setDateType("custom"); setDateType("custom");
setDateRange({ start: "", end: "" }); setDateRange({ start: "", end: "" });
setVisible(true);
break; break;
} }
}; };
@@ -74,23 +102,136 @@ const DownloadBill: React.FC = () => {
end: dayjs(end).format("YYYY-MM-DD"), end: dayjs(end).format("YYYY-MM-DD"),
}); });
}; };
const handlePasswordInput = (e: any) => {
const value = e.detail.value;
const [one = "", two = "", three = "", four = "", five = "", six = ""] =
value.split("");
setPassword([one, two, three, four, five, six]);
if (value.length === 6) {
// const timer = setTimeout(() => {
// // TODO 校验密码
// if (false) {
// set_show_download_popup(false);
// Taro.showModal({
// content: "支付密码错误,请重试",
// cancelText: "忘记密码",
// confirmText: "重试",
// cancelColor: "#000",
// confirmColor: "#fff",
// }).then((res) => {
// if (res.confirm) {
// set_show_download_popup(true);
// } else if (res.cancel) {
// Taro.navigateTo({
// url: "/user_pages/validPhone/index"
// });
// }
// }).finally(() => {
// clearTimeout(timer);
// });
// } else {
// // TODO 下载账单
// }
// }, 100);
}
};
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 [load_transactions_params, set_load_transactions_params] =
useState<TransactionLoadParams>({
transaction_sub_type: TransactionSubType.All,
date_range: [],
});
const handleClose = () => {
setTransactionSubType(load_transactions_params.transaction_sub_type);
setShowFilterPopup(false);
};
const handleTypeConfirm = () => {
set_load_transactions_params((prev) => {
return { ...prev, transaction_sub_type: transactionSubType };
});
setShowFilterPopup(false);
};
const handleDownloadBill = async () => {
try {
const { transaction_sub_type } = load_transactions_params;
const { start, end } = dateRange;
const date_range = [start, end];
const res = await httpService.post("/wallet/download_bill", { transaction_sub_type, date_range });
const { fileUrl, fileName } = res.data;
// 调用下载文件接口
wx.downloadFile({
url: fileUrl, // 文件路径
success: function (res) {
// 只有200状态码表示下载成功
if (res.statusCode === 200) {
// 下载成功后可以使用res.tempFilePath访问临时文件路径
console.log('文件下载成功,临时路径为:', res.tempFilePath);
// 保存文件到本地
wx.openDocument({
filePath: res.tempFilePath,
showMenu: true // 显示保存菜单
});
}
},
fail: function (err) {
console.error('文件下载失败:', err);
}
});
} catch (error) {
console.error(error);
}
};
return ( return (
<View className="download_bill_page"> <View className="download_bill_page">
<View className="hint_content"> <View className="hint_content">
<Text> </Text> <Text> </Text>
<Text className="button_text"></Text> {/* <Text className="button_text">示例文件</Text> */}
</View> </View>
<View className="form_container"> <View className="form_container">
<View className="form_item"> {/* <View className="form_item">
<Text className="title_text">接收方式</Text> <Text className="title_text">接收方式</Text>
<View className="value_content arrow"> <View className="value_content arrow">
<Text>小程序消息</Text> <Text>小程序消息</Text>
</View> </View>
</View> </View> */}
<View className="form_item"> <View
className="form_item"
onClick={() => {
setShowFilterPopup(true);
}}
>
<Text className="title_text"></Text> <Text className="title_text"></Text>
<View className="value_content arrow"> <View className="value_content arrow">
<Text></Text> <Text>
{
transaction_type_options.find(
(item) =>
item.value === load_transactions_params.transaction_sub_type
)?.label
}
</Text>
</View> </View>
</View> </View>
<View className="form_item"> <View className="form_item">
@@ -105,9 +246,8 @@ const DownloadBill: React.FC = () => {
</View> </View>
<View <View
className={`option_button ${ className={`option_button ${dateType === "month" ? "active" : ""
dateType === "month" ? "active" : "" }`}
}`}
onClick={() => { onClick={() => {
selectDateRange("month"); selectDateRange("month");
}} }}
@@ -115,9 +255,8 @@ const DownloadBill: React.FC = () => {
</View> </View>
<View <View
className={`option_button ${ className={`option_button ${dateType === "custom" ? "active" : ""
dateType === "custom" ? "active" : "" }`}
}`}
onClick={() => { onClick={() => {
selectDateRange("custom"); selectDateRange("custom");
}} }}
@@ -126,11 +265,28 @@ const DownloadBill: React.FC = () => {
</View> </View>
</View> </View>
</View> </View>
{dateRange.start && dateRange.end && ( {dateRange.start && dateRange.end && dateType !== "custom" && (
<View className="time_box"> <View className="time_box">
<Text>{dateRange.start}</Text> <Text>{dateRange.end}</Text> <Text>{dateRange.start}</Text> <Text>{dateRange.end}</Text>
</View> </View>
)} )}
{dateType === "custom" && (
<View
className="form_item"
onClick={() => {
setVisible(true);
}}
>
<Text className="title_text"></Text>
<View className="value_content arrow">
<Text>
{dateRange.start && dateRange.end
? `${dateRange.start}${dateRange.end}`
: "请选择账单时间"}
</Text>
</View>
</View>
)}
</View> </View>
<View className="button_container"> <View className="button_container">
<Text <Text
@@ -141,7 +297,9 @@ const DownloadBill: React.FC = () => {
> >
</Text> </Text>
<Button className="download_button"></Button> <Button className="download_button" onClick={handleDownloadBill}>
</Button>
</View> </View>
{visible && ( {visible && (
<DialogCalendarCard <DialogCalendarCard
@@ -152,6 +310,72 @@ const DownloadBill: React.FC = () => {
onClose={() => setVisible(false)} onClose={() => setVisible(false)}
/> />
)} )}
{/* 下载账单输入密码弹窗 */}
<CommonPopup
visible={show_download_popup}
onClose={() => set_show_download_popup(false)}
title="提现"
className="withdraw_popup"
hideFooter={true}
>
<View className="popup_content">
<View className="popup_text">{`支付账单流水文件(文件名).xlsx`}</View>
<View className="popup_text">{`文件大小7KB`}</View>
<View className="popup_text">{`请输入交易密码`}</View>
<View className="password_container">
{password.map((item, index) => (
<View key={index} className="password_item">
<Text className="password_text">{item}</Text>
</View>
))}
</View>
<Input
focus={isFocus}
type="number"
style={{ width: "0", height: "0", opacity: "0" }}
value={password.filter((item) => item !== "").join("")}
maxlength={6}
onInput={handlePasswordInput}
/>
</View>
</CommonPopup>
{/* 筛选账单弹窗 */}
<CommonPopup
visible={showFilterPopup}
onClose={handleClose}
onConfirm={handleTypeConfirm}
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">
{transaction_type_options.map(
(option: Option<TransactionSubType>) => (
<View
className={
transactionSubType === option.value
? "option_item active"
: "option_item"
}
key={option.value}
onClick={() => {
setTransactionSubType(option.value);
}}
>
{option.label}
</View>
)
)}
</View>
</View>
</View>
</View>
</CommonPopup>
</View> </View>
); );
}; };

View File

@@ -0,0 +1,64 @@
.download-bill-records-page {
color: #3C3C4399;
font-family: PingFang SC;
font-weight: 400;
font-style: Regular;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
padding: 20px;
.records-container {
.record-item {
padding: 16px 0;
&+.record-item {
border-top: 1px solid #0000000D;
}
.title-text {
font-size: 16px;
color: #000;
margin-bottom: 8px;
}
.info-item {
display: flex;
gap: 20px;
&+.info-item {
margin-top: 8px;
}
Text {
&:first-child {
width: 64px;
}
&:last-child {
flex: 1;
color: #000;
&.btn {
color: #007AFF;
flex: unset;
width: fit-content;
}
}
}
}
}
}
.tips {
font-size: 12px;
text-align: center;
position: fixed;
bottom: 40px;
width: calc(100% - 40px);
}
}

View File

@@ -1,9 +1,75 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { View } from "@tarojs/components"; import { View, Text } from "@tarojs/components";
import "./index.scss";
import httpService from "@/services/httpService";
import Taro from "@tarojs/taro";
interface BillRecord {
id: number;
file_name: string;
download_url: string;
file_size: number;
create_time: string;
expire_time: string;
bill_date_range_start: string;
bill_date_range_end: string;
bill_transaction_type: string;
bill_transaction_sub_type: string;
date_range_desc: string;
transaction_type_desc: string;
transaction_sub_type_desc: string;
}
const DownloadBillRecords: React.FC = () => { const DownloadBillRecords: React.FC = () => {
const [records, setRecords] = useState<BillRecord[]>([]);
const [params, setParams] = useState({
page: 1,
limit: 20,
});
useEffect(() => {
fetchRecords();
}, []);
const fetchRecords = async () => {
try {
const response = await httpService.post<{ rows: BillRecord[] }>('/wallet/download_history', params);
setRecords(response.data.rows);
} catch (error) {
console.log(error);
Taro.showToast({
title: '获取账单记录失败',
icon: 'none',
duration: 2000,
});
}
};
return ( return (
<View></View> <View className="download-bill-records-page">
<View className="records-container">
{
records.map((record) => (
<View className="record-item" key={record.id}>
<View className="title-text">{record.file_name}</View>
<View className="info-item">
<Text></Text>
<Text>{record.create_time}</Text>
</View>
<View className="info-item">
<Text></Text>
<Text>{record.date_range_desc}</Text>
</View>
<View className="info-item">
<Text></Text>
<Text className="btn"></Text>
</View>
</View>
))
}
</View>
<View className="tips">7</View>
</View>
); );
}; };

View File

@@ -1,50 +1,50 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { View, Text, Image, ScrollView, Button } from '@tarojs/components'; import { View, Text, Image, ScrollView, Button } from "@tarojs/components";
import { PopupPicker } from '@/components/Picker/index' import { PopupPicker } from "@/components/Picker/index";
import Taro from '@tarojs/taro'; import Taro from "@tarojs/taro";
import './index.scss'; import "./index.scss";
import { UserInfo } from '@/components/UserInfo'; import { UserInfo } from "@/components/UserInfo";
import { UserService } from '@/services/userService'; import { UserService, PickerOption } from "@/services/userService";
import { clear_login_state } from '@/services/loginService'; import { clear_login_state } from "@/services/loginService";
import { convert_db_gender_to_display } from '@/utils/genderUtils'; import { convert_db_gender_to_display } from "@/utils/genderUtils";
import { EditModal } from '@/components'; import { EditModal } from "@/components";
import img from "@/config/images"; import img from "@/config/images";
const EditProfilePage: React.FC = () => { const EditProfilePage: React.FC = () => {
// 用户信息状态 // 用户信息状态
const [user_info, setUserInfo] = useState<UserInfo>({ const [user_info, setUserInfo] = useState<UserInfo>({
id: '1', id: "1",
nickname: '加载中...', nickname: "加载中...",
avatar: require('@/static/userInfo/default_avatar.svg'), avatar: require("@/static/userInfo/default_avatar.svg"),
join_date: '加载中...', join_date: "加载中...",
stats: { stats: {
following: 0, following: 0,
friends: 0, friends: 0,
hosted: 0, hosted: 0,
participated: 0 participated: 0,
}, },
personal_profile: '加载中...', personal_profile: "加载中...",
occupation: '加载中...', occupation: "加载中...",
ntrp_level: 'NTRP 3.0', ntrp_level: "NTRP 3.0",
phone: '', phone: "",
gender: '', gender: "",
country: '', country: "",
province: '', province: "",
city: '', city: "",
}); });
// 表单状态 // 表单状态
const [form_data, setFormData] = useState({ const [form_data, setFormData] = useState({
nickname: '', nickname: "",
personal_profile: '', personal_profile: "",
occupation: '', occupation: "",
ntrp_level: '4.0', ntrp_level: "4.0",
phone: '', phone: "",
gender: '', gender: "",
birthday: '2000-01-01', birthday: "2000-01-01",
country: '', country: "",
province: '', province: "",
city: '' city: "",
}); });
// 加载状态 // 加载状态
@@ -52,18 +52,44 @@ const EditProfilePage: React.FC = () => {
// 编辑弹窗状态 // 编辑弹窗状态
const [edit_modal_visible, setEditModalVisible] = useState(false); const [edit_modal_visible, setEditModalVisible] = useState(false);
const [editing_field, setEditingField] = useState<string>(''); const [editing_field, setEditingField] = useState<string>("");
const [gender_picker_visible, setGenderPickerVisible] = useState(false); const [gender_picker_visible, setGenderPickerVisible] = useState(false);
const [birthday_picker_visible, setBirthdayPickerVisible] = useState(false); const [birthday_picker_visible, setBirthdayPickerVisible] = useState(false);
const [location_picker_visible, setLocationPickerVisible] = useState(false); const [location_picker_visible, setLocationPickerVisible] = useState(false);
const [ntrp_picker_visible, setNtrpPickerVisible] = useState(false); const [ntrp_picker_visible, setNtrpPickerVisible] = useState(false);
const [occupation_picker_visible, setOccupationPickerVisible] = useState(false); const [occupation_picker_visible, setOccupationPickerVisible] =
useState(false);
// 职业数据
const [professions, setProfessions] = useState<PickerOption[]>([]);
// 城市数据
const [cities, setCities] = useState<PickerOption[]>([]);
// 页面加载时初始化数据 // 页面加载时初始化数据
useEffect(() => { useEffect(() => {
load_user_info(); load_user_info();
getProfessions();
getCities();
}, []); }, []);
const getProfessions = async () => {
try {
const res = await UserService.getProfessions();
setProfessions(res);
} catch (e) {
console.log("获取职业失败:", e);
}
};
const getCities = async () => {
try {
const res = await UserService.getCities();
setCities(res);
} catch (e) {
console.log("获取职业失败:", e);
}
};
// 加载用户信息 // 加载用户信息
const load_user_info = async () => { const load_user_info = async () => {
try { try {
@@ -71,23 +97,23 @@ const EditProfilePage: React.FC = () => {
const user_data = await UserService.get_user_info(); const user_data = await UserService.get_user_info();
setUserInfo(user_data); setUserInfo(user_data);
setFormData({ setFormData({
nickname: user_data.nickname || '', nickname: user_data.nickname || "",
personal_profile: user_data.personal_profile || '', personal_profile: user_data.personal_profile || "",
occupation: user_data.occupation || '', occupation: user_data.occupation || "",
ntrp_level: user_data.ntrp_level || 'NTRP 4.0', ntrp_level: user_data.ntrp_level || "NTRP 4.0",
phone: user_data.phone || '', phone: user_data.phone || "",
gender: user_data.gender || '', gender: user_data.gender || "",
birthday: user_data.birthday || '', birthday: user_data.birthday || "",
country: user_data.country || '', country: user_data.country || "",
province: user_data.province || '', province: user_data.province || "",
city: user_data.city || '' city: user_data.city || "",
}); });
} catch (error) { } catch (error) {
console.error('加载用户信息失败:', error); console.error("加载用户信息失败:", error);
Taro.showToast({ Taro.showToast({
title: '加载用户信息失败', title: "加载用户信息失败",
icon: 'error', icon: "error",
duration: 2000 duration: 2000,
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -98,51 +124,51 @@ const EditProfilePage: React.FC = () => {
const handle_avatar_upload = () => { const handle_avatar_upload = () => {
Taro.chooseImage({ Taro.chooseImage({
count: 1, count: 1,
sizeType: ['compressed'], sizeType: ["compressed"],
sourceType: ['album', 'camera'], sourceType: ["album", "camera"],
success: async (res) => { success: async (res) => {
const tempFilePath = res.tempFilePaths[0]; const tempFilePath = res.tempFilePaths[0];
try { try {
const avatar_url = await UserService.upload_avatar(tempFilePath); const avatar_url = await UserService.upload_avatar(tempFilePath);
setUserInfo(prev => ({ ...prev, avatar: avatar_url })); setUserInfo((prev) => ({ ...prev, avatar: avatar_url }));
Taro.showToast({ Taro.showToast({
title: '头像上传成功', title: "头像上传成功",
icon: 'success' icon: "success",
}); });
} catch (error) { } catch (error) {
console.error('头像上传失败:', error); console.error("头像上传失败:", error);
Taro.showToast({ Taro.showToast({
title: '头像上传失败', title: "头像上传失败",
icon: 'none' icon: "none",
}); });
} }
} },
}); });
}; };
// 处理编辑弹窗 // 处理编辑弹窗
const handle_open_edit_modal = (field: string) => { const handle_open_edit_modal = (field: string) => {
if (field === 'gender') { if (field === "gender") {
setGenderPickerVisible(true); setGenderPickerVisible(true);
return; return;
} }
if (field === 'birthday') { if (field === "birthday") {
setBirthdayPickerVisible(true); setBirthdayPickerVisible(true);
return; return;
} }
if (field === 'location') { if (field === "location") {
setLocationPickerVisible(true); setLocationPickerVisible(true);
return; return;
} }
if (field === 'ntrp_level') { if (field === "ntrp_level") {
setNtrpPickerVisible(true); setNtrpPickerVisible(true);
return; return;
} }
if (field === 'occupation') { if (field === "occupation") {
setOccupationPickerVisible(true); setOccupationPickerVisible(true);
return; return;
} }
if (field === 'nickname') { if (field === "nickname") {
// 手动输入 // 手动输入
setEditingField(field); setEditingField(field);
setEditModalVisible(true); setEditModalVisible(true);
@@ -159,60 +185,67 @@ const EditProfilePage: React.FC = () => {
await UserService.update_user_info(update_data); await UserService.update_user_info(update_data);
// 更新本地状态 // 更新本地状态
setFormData(prev => ({ ...prev, [editing_field]: value })); setFormData((prev) => ({ ...prev, [editing_field]: value }));
setUserInfo(prev => ({ ...prev, [editing_field]: value })); setUserInfo((prev) => ({ ...prev, [editing_field]: value }));
// 关闭弹窗 // 关闭弹窗
setEditModalVisible(false); setEditModalVisible(false);
setEditingField(''); setEditingField("");
// 显示成功提示 // 显示成功提示
Taro.showToast({ Taro.showToast({
title: '保存成功', title: "保存成功",
icon: 'success' icon: "success",
}); });
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error("保存失败:", error);
Taro.showToast({ Taro.showToast({
title: '保存失败', title: "保存失败",
icon: 'error' icon: "error",
}); });
} }
}; };
const handle_edit_modal_cancel = () => { const handle_edit_modal_cancel = () => {
setEditModalVisible(false); setEditModalVisible(false);
setEditingField(''); setEditingField("");
}; };
// 处理字段编辑 // 处理字段编辑
const handle_field_edit = async (field: string | { [key: string]: string }, value?: string) => { const handle_field_edit = async (
field: string | { [key: string]: string },
value?: string
) => {
try { try {
if (typeof field === 'object' && field !== null && !Array.isArray(field)) { if (
typeof field === "object" &&
field !== null &&
!Array.isArray(field)
) {
await UserService.update_user_info({ ...field }); await UserService.update_user_info({ ...field });
// 更新本地状态 // 更新本地状态
setFormData(prev => ({ ...prev, ...field })); setFormData((prev) => ({ ...prev, ...field }));
setUserInfo(prev => ({ ...prev, ...field })); setUserInfo((prev) => ({ ...prev, ...field }));
} else { } else {
// 调用更新用户信息接口,只传递修改的字段 // 调用更新用户信息接口,只传递修改的字段
const update_data = { [field as string]: value }; const update_data = { [field as string]: value };
await UserService.update_user_info(update_data); await UserService.update_user_info(update_data);
// 更新本地状态 // 更新本地状态
setFormData(prev => ({ ...prev, [field as string]: value })); setFormData((prev) => ({ ...prev, [field as string]: value }));
setUserInfo(prev => ({ ...prev, [field as string]: value })); setUserInfo((prev) => ({ ...prev, [field as string]: value }));
} }
// 显示成功提示 // 显示成功提示
Taro.showToast({ Taro.showToast({
title: '保存成功', title: "保存成功",
icon: 'success' icon: "success",
}); });
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error("保存失败:", error);
Taro.showToast({ Taro.showToast({
title: '保存失败', title: "保存失败",
icon: 'error' icon: "error",
}); });
} }
}; };
@@ -220,13 +253,19 @@ const EditProfilePage: React.FC = () => {
// 处理性别选择 // 处理性别选择
const handle_gender_change = (e: any) => { const handle_gender_change = (e: any) => {
const gender_value = e[0]; const gender_value = e[0];
handle_field_edit('gender', gender_value); handle_field_edit("gender", gender_value);
}; };
// 处理生日选择 // 处理生日选择
const handle_birthday_change = (e: any) => { const handle_birthday_change = (e: any) => {
const [year, month, day] = e; const [year, month, day] = e;
handle_field_edit('birthday', `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); handle_field_edit(
"birthday",
`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(
2,
"0"
)}`
);
}; };
// 处理地区选择 // 处理地区选择
@@ -238,61 +277,66 @@ const EditProfilePage: React.FC = () => {
// 处理NTRP水平选择 // 处理NTRP水平选择
const handle_ntrp_level_change = (e: any) => { const handle_ntrp_level_change = (e: any) => {
const ntrp_level_value = e[0]; const ntrp_level_value = e[0];
handle_field_edit('ntrp_level', ntrp_level_value); handle_field_edit("ntrp_level", ntrp_level_value);
}; };
// 处理职业选择 // 处理职业选择
const handle_occupation_change = (e: any) => { const handle_occupation_change = (e: any) => {
const [country, province] = e; const [country, province] = e;
handle_field_edit('occupation', `${country} ${province}`); handle_field_edit("occupation", `${country} ${province}`);
}; };
// 处理退出登录 // 处理退出登录
const handle_logout = () => { const handle_logout = () => {
Taro.showModal({ Taro.showModal({
title: '确认退出', title: "确认退出",
content: '确定要退出登录吗?', content: "确定要退出登录吗?",
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 清除用户数据 // 清除用户数据
clear_login_state(); clear_login_state();
Taro.reLaunch({ Taro.reLaunch({
url: '/login_pages/index/index' url: "/login_pages/index/index",
}); });
} }
} },
}); });
}; };
const onGetPhoneNumber = async (e) => { const onGetPhoneNumber = async (e) => {
if (!e.detail || !e.detail.code) { if (!e.detail || !e.detail.code) {
Taro.showToast({ Taro.showToast({
title: '获取手机号失败,请重试', title: "获取手机号失败,请重试",
icon: 'none', icon: "none",
duration: 2000 duration: 2000,
}); });
return; return;
} }
try { try {
const phone = await UserService.parse_phone(e.detail.code); const phone = await UserService.parse_phone(e.detail.code);
handle_field_edit('phone', phone); handle_field_edit("phone", phone);
} catch (e) { } catch (e) {
console.error('解析手机号失败:', e); console.error("解析手机号失败:", e);
Taro.showToast({ Taro.showToast({
title: '解析手机号失败,请重试', title: "解析手机号失败,请重试",
icon: 'none', icon: "none",
duration: 2000 duration: 2000,
}); });
} }
} };
return ( return (
<View className="edit_profile_page"> <View className="edit_profile_page">
{/* 导航栏 */} {/* 导航栏 */}
<View className="custom-navbar"> <View className="custom-navbar">
<View className="detail-navigator"> <View className="detail-navigator">
<View className="detail-navigator-back" onClick={() => { Taro.navigateBack() }}> <View
className="detail-navigator-back"
onClick={() => {
Taro.navigateBack();
}}
>
<Image <Image
className="detail-navigator-back-icon" className="detail-navigator-back-icon"
src={img.ICON_NAVIGATOR_BACK} src={img.ICON_NAVIGATOR_BACK}
@@ -315,26 +359,35 @@ const EditProfilePage: React.FC = () => {
<View className="avatar_overlay"> <View className="avatar_overlay">
<Image <Image
className="upload_icon" className="upload_icon"
src={require('@/static/userInfo/edit2.svg')} src={require("@/static/userInfo/edit2.svg")}
/> />
</View> </View>
</View> </View>
</View> </View>
{/* 基本信息编辑 */} {/* 基本信息编辑 */}
<View className="form_section"> <View className="form_section">
{/* 名字 */} {/* 名字 */}
<View className="form_group"> <View className="form_group">
<View className="form_item" onClick={() => handle_open_edit_modal('nickname')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("nickname")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/user.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/user.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{form_data.nickname || '188的王晨'}</Text> <Text className="item_value">
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> {form_data.nickname || "188的王晨"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
<View className="divider"></View> <View className="divider"></View>
@@ -342,14 +395,25 @@ const EditProfilePage: React.FC = () => {
{/* 性别 */} {/* 性别 */}
<View className="form_group"> <View className="form_group">
<View className="form_item" onClick={() => handle_open_edit_modal('gender')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("gender")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/gender.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/gender.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{convert_db_gender_to_display(form_data.gender)}</Text> <Text className="item_value">
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> {convert_db_gender_to_display(form_data.gender)}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
<View className="divider"></View> <View className="divider"></View>
@@ -357,14 +421,23 @@ const EditProfilePage: React.FC = () => {
{/* 生日 */} {/* 生日 */}
<View className="form_group"> <View className="form_group">
<View className="form_item" onClick={() => handle_open_edit_modal('birthday')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("birthday")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/birthday.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/birthday.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{form_data.birthday}</Text> <Text className="item_value">{form_data.birthday}</Text>
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> <Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
</View> </View>
@@ -373,16 +446,26 @@ const EditProfilePage: React.FC = () => {
{/* 简介编辑 */} {/* 简介编辑 */}
<View className="form_section"> <View className="form_section">
<View className="form_group"> <View className="form_group">
<View className="form_item" onClick={() => handle_open_edit_modal('personal_profile')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("personal_profile")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/introduce.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/introduce.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value"> <Text className="item_value">
{form_data.personal_profile.replace(/\n/g, ' ') || '介绍一下自己'} {form_data.personal_profile.replace(/\n/g, " ") ||
"介绍一下自己"}
</Text> </Text>
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> <Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
</View> </View>
@@ -392,40 +475,67 @@ const EditProfilePage: React.FC = () => {
<View className="form_section"> <View className="form_section">
<View className="form_group"> <View className="form_group">
{/* 地区 */} {/* 地区 */}
<View className="form_item" onClick={() => handle_open_edit_modal('location')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("location")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/gender.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/gender.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{`${form_data.country} ${form_data.province} ${form_data.city}`}</Text> <Text className="item_value">{`${form_data.country} ${form_data.province} ${form_data.city}`}</Text>
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> <Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
<View className="divider"></View> <View className="divider"></View>
{/* NTRP水平 */} {/* NTRP水平 */}
<View className="form_item" onClick={() => handle_open_edit_modal('ntrp_level')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("ntrp_level")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/ball.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/ball.svg")}
/>
<Text className="item_label">NTRP </Text> <Text className="item_label">NTRP </Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{form_data.ntrp_level}</Text> <Text className="item_value">{form_data.ntrp_level}</Text>
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> <Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
<View className="divider"></View> <View className="divider"></View>
{/* 职业 */} {/* 职业 */}
<View className="form_item" onClick={() => handle_open_edit_modal('occupation')}> <View
className="form_item"
onClick={() => handle_open_edit_modal("occupation")}
>
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/business.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/business.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
<Text className="item_value">{form_data.occupation}</Text> <Text className="item_value">{form_data.occupation}</Text>
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> <Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
</View> </View>
@@ -436,7 +546,10 @@ const EditProfilePage: React.FC = () => {
<View className="form_group"> <View className="form_group">
<View className="form_item"> <View className="form_item">
<View className="item_left"> <View className="item_left">
<Image className="item_icon" src={require('@/static/userInfo/phone.svg')} /> <Image
className="item_icon"
src={require("@/static/userInfo/phone.svg")}
/>
<Text className="item_label"></Text> <Text className="item_label"></Text>
</View> </View>
<View className="item_right"> <View className="item_right">
@@ -448,8 +561,17 @@ const EditProfilePage: React.FC = () => {
onInput={handle_phone_input} onInput={handle_phone_input}
onBlur={handle_phone_blur} onBlur={handle_phone_blur}
/> */} /> */}
<Button className={form_data.phone ? '' : 'placeholer'} openType='getPhoneNumber' onGetPhoneNumber={onGetPhoneNumber}>{form_data.phone || '未绑定'}</Button> <Button
<Image className="arrow_icon" src={require('@/static/list/icon-list-right-arrow.svg')} /> className={form_data.phone ? "" : "placeholer"}
openType="getPhoneNumber"
onGetPhoneNumber={onGetPhoneNumber}
>
{form_data.phone || "未绑定"}
</Button>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View> </View>
</View> </View>
<View className="divider"></View> <View className="divider"></View>
@@ -477,63 +599,94 @@ const EditProfilePage: React.FC = () => {
<EditModal <EditModal
visible={edit_modal_visible} visible={edit_modal_visible}
type={editing_field} type={editing_field}
title={editing_field === 'nickname' ? '编辑名字' : '编辑简介'} title={editing_field === "nickname" ? "编辑名字" : "编辑简介"}
placeholder={editing_field === 'nickname' ? '请输入您的名字' : '介绍一下你的喜好,或者训练习惯'} placeholder={
initialValue={form_data[editing_field as keyof typeof form_data] || ''} editing_field === "nickname"
maxLength={editing_field === 'nickname' ? 20 : 100} ? "请输入您的名字"
: "介绍一下你的喜好,或者训练习惯"
}
initialValue={form_data[editing_field as keyof typeof form_data] || ""}
maxLength={editing_field === "nickname" ? 20 : 100}
onSave={handle_edit_modal_save} onSave={handle_edit_modal_save}
onCancel={handle_edit_modal_cancel} onCancel={handle_edit_modal_cancel}
validationMessage={editing_field === 'nickname' ? '请填写 1-20 个字符' : '请填写 2-100 个字符'} validationMessage={
editing_field === "nickname"
? "请填写 1-20 个字符"
: "请填写 2-100 个字符"
}
/> />
{/* 性别选择弹窗 */} {/* 性别选择弹窗 */}
{gender_picker_visible && <PopupPicker {gender_picker_visible && (
options={[ <PopupPicker
[{ text: '男', value: '0' }, options={[
{ text: '女', value: '1' }, [
{ text: '保密', value: '2' } { text: "男", value: "0" },
]]} { text: "女", value: "1" },
visible={gender_picker_visible} { text: "保密", value: "2" },
setvisible={setGenderPickerVisible} ],
value={[form_data.gender]} ]}
onChange={handle_gender_change} />} visible={gender_picker_visible}
setvisible={setGenderPickerVisible}
value={[form_data.gender]}
onChange={handle_gender_change}
/>
)}
{/* 生日选择弹窗 */} {/* 生日选择弹窗 */}
{birthday_picker_visible && <PopupPicker {birthday_picker_visible && (
visible={birthday_picker_visible} <PopupPicker
setvisible={setBirthdayPickerVisible} visible={birthday_picker_visible}
value={[new Date(form_data.birthday).getFullYear(), new Date(form_data.birthday).getMonth() + 1, new Date(form_data.birthday).getDate()]} setvisible={setBirthdayPickerVisible}
type="day" value={[
onChange={handle_birthday_change} />} new Date(form_data.birthday).getFullYear(),
new Date(form_data.birthday).getMonth() + 1,
new Date(form_data.birthday).getDate(),
]}
type="day"
onChange={handle_birthday_change}
/>
)}
{/* 地区选择弹窗 */} {/* 地区选择弹窗 */}
{location_picker_visible && <PopupPicker {location_picker_visible && (
options={[[{ text: "中国", value: "中国" }], [{ text: "上海", value: "上海" }], [{ text: "浦东新区", value: "浦东新区" }, {text: "静安区", value: "静安区"}]]} <PopupPicker
visible={location_picker_visible} options={cities}
setvisible={setLocationPickerVisible} visible={location_picker_visible}
value={[form_data.country, form_data.province, form_data.city]} setvisible={setLocationPickerVisible}
onChange={handle_location_change} />} value={[form_data.country, form_data.province, form_data.city]}
onChange={handle_location_change}
/>
)}
{/* NTRP水平选择弹窗 */} {/* NTRP水平选择弹窗 */}
{ntrp_picker_visible && <PopupPicker {ntrp_picker_visible && (
options={[ <PopupPicker
[{ text: '1.5', value: '1.5' }, options={[
{ text: '2.0', value: '2.0' }, [
{ text: '2.5', value: '2.5' }, { text: "1.5", value: "1.5" },
{ text: '3.0', value: '3.0' }, { text: "2.0", value: "2.0" },
{ text: '3.5', value: '3.5' }, { text: "2.5", value: "2.5" },
{ text: '4.0', value: '4.0' }, { text: "3.0", value: "3.0" },
{ text: '4.5', value: '4.5' }, { text: "3.5", value: "3.5" },
]]} { text: "4.0", value: "4.0" },
type="ntrp" { text: "4.5", value: "4.5" },
img={user_info.avatar} ],
visible={ntrp_picker_visible} ]}
setvisible={setNtrpPickerVisible} type="ntrp"
value={[form_data.ntrp_level]} img={user_info.avatar}
onChange={handle_ntrp_level_change} />} visible={ntrp_picker_visible}
setvisible={setNtrpPickerVisible}
value={[form_data.ntrp_level]}
onChange={handle_ntrp_level_change}
/>
)}
{/* 职业选择弹窗 */} {/* 职业选择弹窗 */}
{occupation_picker_visible && <PopupPicker {occupation_picker_visible && (
options={[[{ text: "时尚", value: "时尚" }], [{ text: "美妆博主", value: "美妆博主" },{ text: "设计师", value: "设计师" }]]} <PopupPicker
visible={occupation_picker_visible} options={professions}
setvisible={setOccupationPickerVisible} visible={occupation_picker_visible}
value={[...form_data.occupation.split(' ')]} setvisible={setOccupationPickerVisible}
onChange={handle_occupation_change} />} value={[...form_data.occupation.split(" ")]}
onChange={handle_occupation_change}
/>
)}
</View> </View>
); );
}; };

View File

@@ -1,197 +1,206 @@
.listSearchContainer { .listSearchContainer {
padding: 0 15px; padding: 0 15px;
padding-top: 16px; padding-top: 16px;
.icon16 { .icon16 {
width: 16px; width: 16px;
height: 16px; height: 16px;
}
.topSearch {
padding: 10px 16px 5px 12px;
display: flex;
align-items: center;
height: 44px;
box-sizing: border-box;
gap: 10px;
border-radius: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
.nut-input {
padding: 0;
height: 100%;
}
}
.searchRight {
display: flex;
align-items: center;
gap: 12px;
.searchLine {
width: 1px;
height: 20px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.06);
} }
.searchText { .topSearch {
color: #000000; padding: 5px 16px 5px 12px;
font-size: 16px; display: flex;
font-weight: 600; align-items: center;
line-height: 20px; height: 44px;
} box-sizing: border-box;
} gap: 10px;
border-radius: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
.searchIcon { .nut-input {
width: 20px; padding: 0;
height: 20px; height: 100%;
} }
.historySearchTitleWrapper {
display: flex;
padding: 12px 15px;
justify-content: space-between;
align-items: flex-end;
align-self: stretch;
.historySearchTitle,
.historySearchClear {
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 20px;
} }
.historySearchClear { .searchRight {
color: #9a9a9a;
display: flex;
align-items: center;
gap: 4px;
}
}
.historySearchList {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.historySearchItem {
flex-shrink: 0;
flex-grow: 0;
display: flex;
height: 28px;
padding: 4px 12px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.03);
}
}
.searchSuggestion {
padding: 6px 0;
.searchSuggestionItem {
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.searchSuggestionItemLeft {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
color: rgba(60, 60, 67, 0.6);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.highlight { .searchLine {
color: #000000; width: 1px;
} height: 20px;
} border-radius: 20px;
} background: rgba(0, 0, 0, 0.06);
.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 { .searchText {
display: flex; color: #000000;
align-items: center; font-size: 16px;
align-self: stretch; font-weight: 600;
gap: 4px; line-height: 20px;
.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 { .searchIcon {
width: 20px;
height: 20px;
}
.historySearchTitleWrapper {
display: flex; display: flex;
flex-direction: column; padding: 12px 0;
justify-content: space-between;
align-items: flex-end; align-items: flex-end;
gap: 4px; align-self: stretch;
width: 68px;
.transaction_amount { .historySearchTitle,
font-size: 12px; .historySearchClear {
font-weight: 600; color: #000;
color: #000; font-size: 14px;
line-height: 1.5; font-weight: 600;
text-align: right; line-height: 20px;
} }
.balance_info { .historySearchClear {
font-size: 10px; color: #9a9a9a;
font-weight: 400; display: flex;
color: rgba(60, 60, 67, 0.6); align-items: center;
line-height: 1.2; gap: 4px;
text-align: right;
} }
}
} }
}
.historySearchList {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.historySearchItem {
color: #3C3C4399;
font-size: 12px;
flex-shrink: 0;
flex-grow: 0;
display: flex;
padding: 4px 12px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.03);
}
}
.searchSuggestion {
padding: 6px 0;
.searchSuggestionItem {
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.searchSuggestionItemLeft {
display: flex;
align-items: center;
gap: 12px;
color: rgba(60, 60, 67, 0.6);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.highlight {
color: #000000;
}
}
}
.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: 12px 0;
.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;
}
}
}
}
.tips_text {
font-size: 12px;
text-align: center;
color: #3C3C4399;
margin-top: 20px;
}
} }

View File

@@ -341,6 +341,11 @@ const QueryTransactions = () => {
</View> </View>
)} )}
</View> </View>
{
transactions.length > 0 && (
<View className="tips_text">202491</View>
)
}
</View> </View>
</> </>
); );

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '设置交易密码',
})

View File

@@ -0,0 +1,60 @@
.set-transaction-password-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
.form-item {
height: 50px;
display: flex;
gap: 10px;
align-items: center;
border-bottom: 1px solid #0000000D;
font-size: 14px;
.form-label {
width: 56px;
text-align: right;
}
}
.tips {
font-family: PingFang SC;
font-weight: 400;
font-style: Regular;
font-size: 12px;
line-height: 18px;
letter-spacing: 0px;
vertical-align: middle;
color: #3C3C4366;
}
.btn {
height: 24px;
border: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 9.6px;
font-style: normal;
line-height: normal;
border-radius: 8px;
margin-right: 0;
}
.bottom-btn {
position: fixed;
bottom: 40px;
height: 54px;
width: calc(100vw - 40px);
margin: 0 auto;
border-radius: 16px;
font-size: 16px;
font-weight: 600;
}
}

View File

@@ -0,0 +1,114 @@
import React, { useState, useEffect } from "react";
import Taro, { useRouter } from '@tarojs/taro';
import { View, Text, Input, Button } from "@tarojs/components";
import "./index.scss";
import httpService from "@/services/httpService";
interface FormFields {
old_password?: string;
new_password: string;
confirm_password: string;
sms_code?: string;
}
const SetTransactionPassword: React.FC = () => {
const [handleType, setHandleType] = useState("set");
const router = useRouter();
const { type, phone, sms_code } = router.params;
useEffect(() => {
if (type) {
setHandleType(type);
}
}, [type]);
const [formData, setFormData] = useState<FormFields>({
old_password: "",
new_password: "",
confirm_password: "",
sms_code: "",
});
const handleInput = (e: any, field: string) => {
setFormData({ ...formData, [field]: e.detail.value });
};
const handleConfirm = async () => {
const { new_password, confirm_password } = formData;
if (new_password !== confirm_password) {
Taro.showToast({
title: "两次密码输入不一致",
icon: "none",
});
return;
}
if (handleType === "set") {
const { sms_code } = formData;
try {
await httpService.post("/wallet/set_payment_password", { password: new_password, sms_code });
Taro.showToast({
title: "设置交易密码成功",
icon: "success",
});
Taro.navigateBack();
} catch (error) {
Taro.showToast({
title: "设置交易密码失败",
icon: "none",
});
return;
}
} else if (handleType === "reset") {
// const { old_password } = formData;
try {
await httpService.post("/wallet/reset_payment_password", { phone, new_password, sms_code });
Taro.showToast({
title: "修改交易密码成功",
icon: "success",
});
Taro.navigateBack();
} catch (error) {
Taro.showToast({
title: "修改交易密码失败",
icon: "none",
});
return;
}
}
};
return (
<View className="set-transaction-password-page">
{
// handleType === "reset" && (
// <View className="form-item">
// <Text className="form-label">旧密码</Text>
// <Input placeholder="请输入旧密码" password type="number" maxlength={6} onInput={(e) => { handleInput(e, "old_password") }}></Input>
// </View>
// )
}
<View className="form-item">
<Text className="form-label"></Text>
<Input placeholder="请输入交易密码" password type="number" maxlength={6} onInput={(e) => { handleInput(e, "new_password") }}></Input>
</View>
<View className="form-item">
<Text className="form-label"></Text>
<Input placeholder="请再次输入交易密码" password type="number" maxlength={6} onInput={(e) => { handleInput(e, "confirm_password") }}></Input>
</View>
{
handleType === "set" && (
<View className="form-item">
<Text className="form-label"></Text>
<Input placeholder="请输入验证码" type="number" onInput={(e) => { handleInput(e, "sms_code") }}></Input>
<Button className="btn" ></Button>
</View>
)
}
<Text className="tips">* 6</Text>
<Button className="btn bottom-btn" onClick={handleConfirm}></Button>
</View>
);
};
export default SetTransactionPassword;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '验证手机号',
})

View File

@@ -0,0 +1,49 @@
.set-transaction-password-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
.form-item {
height: 50px;
display: flex;
gap: 10px;
align-items: center;
border-bottom: 1px solid #0000000D;
font-size: 14px;
.form-label {
width: 56px;
text-align: right;
}
}
.btn {
height: 24px;
border: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 9.6px;
font-style: normal;
line-height: normal;
border-radius: 8px;
margin-right: 0;
}
.bottom-btn {
position: fixed;
bottom: 40px;
height: 54px;
width: calc(100vw - 40px);
margin: 0 auto;
border-radius: 16px;
font-size: 16px;
font-weight: 600;
}
}

View File

@@ -0,0 +1,77 @@
import React, { useState, useEffect } from "react";
import Taro from '@tarojs/taro';
import { View, Text, Input, Button } from "@tarojs/components";
import "./index.scss";
import httpService from "@/services/httpService";
import { useUserInfo } from "@/store/userStore";
interface FormFields {
phone?: string;
sms_code?: string;
}
const ValidPhone: React.FC = () => {
const userInfo = useUserInfo();
const [formData, setFormData] = useState<FormFields>({
phone: userInfo.phone || "",
sms_code: "",
});
const handleInput = (e: any, field: string) => {
setFormData({ ...formData, [field]: e.detail.value });
};
const handleConfirm = async () => {
const isValid = await validSMSCode();
if (isValid) {
Taro.navigateTo({ url: `/user_pages/setTransactionPassword/index?type=reset&phone=${formData.phone}&sms_code=${formData.sms_code}` });
}
};
const validSMSCode = async () => {
const { phone, sms_code } = formData;
try {
const res = await httpService.post("/wallet/verify_sms_code", { phone, sms_code, type: "reset_password" });
const { verified } = res.data;
if (verified) {
return true;
} else {
Taro.showToast({ title: "验证码校验失败", icon: "none" });
return false;
}
} catch (error) {
console.log(error);
Taro.showToast({ title: "验证码校验失败", icon: "none" });
return false;
}
};
const getSMSCode = async () => {
const { phone } = formData;
try {
await httpService.post("/wallet/send_reset_password_sms", { phone });
Taro.showToast({ title: "验证码已发送", icon: "none" });
} catch (error) {
console.log(error);
Taro.showToast({ title: "获取验证码失败", icon: "none" });
}
};
return (
<View className="set-transaction-password-page">
<View className="form-item">
<Text className="form-label"></Text>
<Input defaultValue={formData.phone} type="number" disabled></Input>
</View>
<View className="form-item">
<Text className="form-label"></Text>
<Input placeholder="请输入验证码" type="number" onInput={(e) => { handleInput(e, "sms_code") }}></Input>
<Button className="btn" onClick={getSMSCode}></Button>
</View>
<Button className="btn bottom-btn" disabled={!formData.sms_code} onClick={handleConfirm}></Button>
</View>
);
};
export default ValidPhone;

View File

@@ -146,7 +146,6 @@
border: 0.5px solid #EBEBEB; border: 0.5px solid #EBEBEB;
border-radius: 20px; border-radius: 20px;
box-shadow: 0px 0px 36px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 36px 0px rgba(0, 0, 0, 0.1);
overflow: hidden;
.history_header { .history_header {
display: flex; display: flex;
@@ -154,6 +153,9 @@
align-items: center; align-items: center;
padding: 12px 20px; padding: 12px 20px;
border-bottom: 0.5px solid rgba(120, 120, 128, 0.12); border-bottom: 0.5px solid rgba(120, 120, 128, 0.12);
position: sticky;
top: 0;
background-color: #fff;
.history_title { .history_title {
font-size: 16px; font-size: 16px;

View File

@@ -5,6 +5,7 @@ import "./index.scss";
import { CommonPopup } from "@/components"; import { CommonPopup } from "@/components";
import httpService from "@/services/httpService"; import httpService from "@/services/httpService";
import { withAuth } from "@/components"; import { withAuth } from "@/components";
import { PopupPicker } from "@/components/Picker/index";
// 交易记录类型 // 交易记录类型
interface Transaction { interface Transaction {
@@ -103,6 +104,7 @@ const WalletPage: React.FC = () => {
const [show_withdraw_popup, set_show_withdraw_popup] = useState(false); const [show_withdraw_popup, set_show_withdraw_popup] = useState(false);
const [withdraw_amount, set_withdraw_amount] = useState(""); const [withdraw_amount, set_withdraw_amount] = useState("");
const [submitting, set_submitting] = useState(false); const [submitting, set_submitting] = useState(false);
const [password_status, set_password_status] = useState(false);
// 交易记录状态 // 交易记录状态
const [transactions, set_transactions] = useState<Transaction[]>([]); const [transactions, set_transactions] = useState<Transaction[]>([]);
@@ -110,6 +112,16 @@ const WalletPage: React.FC = () => {
// 交易记录过滤状态 // 交易记录过滤状态
const [showFilterPopup, setShowFilterPopup] = 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] = const [load_transactions_params, set_load_transactions_params] =
useState<TransactionLoadParams>({ useState<TransactionLoadParams>({
@@ -118,15 +130,40 @@ const WalletPage: React.FC = () => {
type: TransactionType.All, type: TransactionType.All,
transaction_sub_type: TransactionSubType.All, transaction_sub_type: TransactionSubType.All,
keyword: "", keyword: "",
date: "", date: `${year}-${month}`
}); });
useEffect(() => {
load_transactions();
}, [load_transactions_params]);
// 页面显示时加载数据 // 页面显示时加载数据
useDidShow(() => { useDidShow(() => {
load_wallet_data(); load_wallet_data();
load_transactions(); check_password_status();
}); });
const modify_load_transactions_params = () => {
const { type, transaction_sub_type } = filterParams;
set_load_transactions_params((prev) => {
return {
...prev,
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 () => { const load_wallet_data = async () => {
try { try {
@@ -171,11 +208,10 @@ const WalletPage: React.FC = () => {
// 加载交易记录 // 加载交易记录
const load_transactions = async () => { const load_transactions = async () => {
setShowFilterPopup(false); setShowFilterPopup(false);
set_load_transactions_params({ ...load_transactions_params, page: 1 }); // set_load_transactions_params({ ...load_transactions_params, page: 1 });
try { try {
set_loading_transactions(true); set_loading_transactions(true);
console.log("开始加载交易记录..."); console.log("开始加载交易记录...");
const response = await httpService.post("/wallet/transactions", { const response = await httpService.post("/wallet/transactions", {
...load_transactions_params, ...load_transactions_params,
}); });
@@ -216,8 +252,24 @@ const WalletPage: React.FC = () => {
} }
}; };
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 = () => { const handle_withdraw = () => {
if (password_status) {
navigateToSetTransactionPassword("set");
return;
}
if (wallet_info.balance <= 0) { if (wallet_info.balance <= 0) {
Taro.showToast({ Taro.showToast({
title: "余额不足", title: "余额不足",
@@ -226,7 +278,10 @@ const WalletPage: React.FC = () => {
}); });
return; return;
} }
set_show_withdraw_popup(true); Taro.navigateTo({
url: "/user_pages/withdrawal/index",
});
// set_show_withdraw_popup(true);
}; };
// 提交提现申请 // 提交提现申请
@@ -297,6 +352,7 @@ const WalletPage: React.FC = () => {
// 格式化时间显示 // 格式化时间显示
const format_time = (time: string) => { const format_time = (time: string) => {
time = time.replace(/-/g, "/");
const date = new Date(time); const date = new Date(time);
const month = String(date.getMonth() + 1).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0");
@@ -350,6 +406,14 @@ const WalletPage: React.FC = () => {
setShowFilterPopup(true); setShowFilterPopup(true);
}; };
const handleFilterCancel = () => {
setShowFilterPopup(false);
setFilterParams({
type: load_transactions_params.type,
transaction_sub_type: load_transactions_params.transaction_sub_type,
});
};
return ( return (
<View className="wallet_page"> <View className="wallet_page">
{/* 钱包主卡片 */} {/* 钱包主卡片 */}
@@ -357,7 +421,7 @@ const WalletPage: React.FC = () => {
{/* 头部信息 */} {/* 头部信息 */}
<View className="card_header"> <View className="card_header">
<Text className="header_title"></Text> <Text className="header_title"></Text>
<Text className="modify_password"></Text> <Text className="modify_password" onClick={() => navigateToSetTransactionPassword("reset")}></Text>
</View> </View>
{/* 余额显示 */} {/* 余额显示 */}
@@ -420,7 +484,8 @@ const WalletPage: React.FC = () => {
/> />
<Text className="function_text"></Text> <Text className="function_text"></Text>
</View> </View>
<View className="function_item"> {/* TODO 客服中心 */}
<View className="function_item" onClick={() => Taro.navigateTo({ url: "/user_pages/validPhone/index" })}>
<Image <Image
className="function_icon" className="function_icon"
src={require("@/static/wallet/custom-service.svg")} src={require("@/static/wallet/custom-service.svg")}
@@ -434,8 +499,8 @@ const WalletPage: React.FC = () => {
{/* 标题栏 */} {/* 标题栏 */}
<View className="history_header"> <View className="history_header">
<Text className="history_title"></Text> <Text className="history_title"></Text>
<View className="month_selector"> <View className="month_selector" onClick={() => setShowMonthPicker(true)}>
<Text className="current_month">2025-09</Text> <Text className="current_month">{load_transactions_params.date}</Text>
</View> </View>
</View> </View>
@@ -526,12 +591,30 @@ const WalletPage: React.FC = () => {
</View> </View>
</View> </View>
</CommonPopup> </CommonPopup>
{/* 选择月份弹窗 */}
{showMonthPicker && (
<PopupPicker
visible={showMonthPicker}
setvisible={setShowMonthPicker}
value={[
Number(load_transactions_params.date!.split("-")[0]),
Number(load_transactions_params.date!.split("-")[1])
]}
type="month"
onChange={(e) => {
const [year, month] = e;
set_load_transactions_params({
...load_transactions_params,
date: `${year}-${String(month).padStart(2, "0")}`,
});
}}
/>
)}
{/* 筛选账单弹窗 */} {/* 筛选账单弹窗 */}
<CommonPopup <CommonPopup
visible={showFilterPopup} visible={showFilterPopup}
onClose={() => setShowFilterPopup(false)} onClose={handleFilterCancel}
onConfirm={load_transactions} onConfirm={modify_load_transactions_params}
title="选择筛选项" title="选择筛选项"
className="filter_popup" className="filter_popup"
> >
@@ -544,14 +627,14 @@ const WalletPage: React.FC = () => {
(option: Option<TransactionType>) => ( (option: Option<TransactionType>) => (
<View <View
className={ className={
load_transactions_params.type === option.value filterParams.type === option.value
? "option_item active" ? "option_item active"
: "option_item" : "option_item"
} }
key={option.value} key={option.value}
onClick={() => { onClick={() => {
set_load_transactions_params({ setFilterParams({
...load_transactions_params, ...filterParams,
type: option.value, type: option.value,
}); });
}} }}
@@ -569,15 +652,15 @@ const WalletPage: React.FC = () => {
(option: Option<TransactionSubType>) => ( (option: Option<TransactionSubType>) => (
<View <View
className={ className={
load_transactions_params.transaction_sub_type === filterParams.transaction_sub_type ===
option.value option.value
? "option_item active" ? "option_item active"
: "option_item" : "option_item"
} }
key={option.value} key={option.value}
onClick={() => { onClick={() => {
set_load_transactions_params({ setFilterParams({
...load_transactions_params, ...filterParams,
transaction_sub_type: option.value, transaction_sub_type: option.value,
}); });
}} }}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '钱包',
})

View File

@@ -0,0 +1,144 @@
.withdrawal-page {
height: 100vh;
font-family: PingFang SC;
font-weight: 400;
font-style: Regular;
color: #3C3C4399;
font-size: 12px;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
.withdrawal-container {
margin: 20px 5px 0;
border-radius: 20px;
padding-top: 12px;
padding-right: 20px;
padding-bottom: 24px;
padding-left: 20px;
gap: 16px;
border: 0.5px solid #EBEBEB;
box-shadow: 0px 4px 36px 0px #0000000D;
.title-text {
color: #000;
}
.input-container {
font-weight: bold;
color: #000;
display: flex;
align-items: flex-end;
gap: 8px;
border-bottom: 0.5px solid var(--Fills-Tertiary, #7878801F);
margin: 12px 0;
padding-bottom: 12px;
.symbol {
font-size: 20px;
}
Input {
font-size: 32px;
overflow: unset;
text-overflow: unset;
height: 32px;
line-height: 32px;
margin: 0;
padding: 0;
border: none;
background: none;
color: inherit;
outline: none;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
flex: 1;
}
.btn {
height: 24px;
border: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 9.6px;
font-style: normal;
line-height: normal;
border-radius: 8px;
margin-right: 0;
}
}
.tips-text {
color: #F3334A;
}
.btn-container {
display: flex;
justify-content: space-between;
.btn {
color: #007AFF;
}
}
}
.tips-container {
padding: 20px 20px;
.title-text {
font-weight: 600;
margin-bottom: 12px;
}
.tips-text {
display: flex;
flex-direction: column;
}
}
}
.withdraw_popup {
.popup_content {
display: flex;
flex-direction: column;
align-items: center;
.popup_text {
font-family: DingTalk JinBuTi;
font-weight: 400;
font-style: Regular;
font-size: 20px;
line-height: 16px;
margin: 20px 0;
}
.password_container {
display: flex;
gap: 4px;
align-items: center;
.password_item {
width: 32px;
height: 32px;
border-radius: 4px;
background-color: #7878801F;
display: flex;
justify-content: center;
align-items: center;
-webkit-text-security: disc;
}
}
}
}

View File

@@ -0,0 +1,283 @@
import React, { useState, useEffect } from "react";
import { View, Text, Input, Button } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro";
import httpService from "@/services/httpService";
import "./index.scss";
import { CommonPopup } from "@/components";
interface WalletInfo {
balance: string;
frozen_balance?: string;
total_balance?: string;
total_income?: string;
total_withdraw?: string;
}
const Withdrawal: React.FC = () => {
const [showTips, setShowTips] = useState(false);
const [tipsText, setTipsText] = useState<string>("");
const [inputValue, setInputValue] = useState<string>("0.00");
const [walletInfo, setWalletInfo] = useState<WalletInfo>({
balance: "0.00",
});
const [isFocus, setIsFocus] = useState(false);
const [show_withdraw_popup, set_show_withdraw_popup] = useState(false);
const [password, setPassword] = useState<string[]>(new Array(6).fill(""));
const toastConfig = {
aa: {
title: "已超单日限额",
content: "您今日提现已超过支付通道2000元单日限额建议您明日再发起提现资金仍安全存放在钱包余额中。"
},
bb: {
title: "已超单日限额",
content: "今日提现通道额度已用完,暂时无法提现;您的余额安全存放在钱包中,我们会在 次日0点恢复 提现服务;如有紧急情况,请联系客服。"
}
}
useDidShow(() => {
load_wallet_data();
});
useEffect(() => {
if (show_withdraw_popup) {
setIsFocus(true);
} else {
setPassword(new Array(6).fill(""));
setIsFocus(false);
}
}, [show_withdraw_popup]);
const validateWithdrawAmount = (amount: string) => {
if (Number(amount) > Number(walletInfo.balance)) {
setShowTips(true);
setTipsText("输入金额超过钱包余额");
} else if (Number(amount) > 200) {
setShowTips(true);
setTipsText("单笔提现金额不能超过 200元");
}
};
const withdrawAll = () => {
setInputValue(walletInfo.balance);
validateWithdrawAmount(walletInfo.balance);
};
const handleInput = (e: any) => {
console.log(e);
const value = e.detail.value;
setInputValue(value);
validateWithdrawAmount(value);
};
// 加载钱包数据
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;
setWalletInfo({
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 handleWithdraw = async () => {
// TODO 校验提现状态
// if (true) {
// Taro.showToast({
// title: "您今日已累计提现 10次达到每日次数上限",
// icon: "none",
// });
// }
// const { aa, bb } = toastConfig;
// Taro.showModal({
// title: aa.title,
// content: aa.content,
// showCancel: false,
// confirmColor: "#000",
// });
// Taro.showModal({
// title: bb.title,
// content: bb.content,
// showCancel: false,
// confirmColor: "#000",
// });
set_show_withdraw_popup(true);
};
const submit_withdraw = async () => {
// 先调用后端接口获取提现参数
const response = await httpService.post("/wallet/withdraw", {
amount: parseFloat(inputValue),
transfer_remark: "用户申请提现",
});
// 根据后端返回的数据结构解析参数
const { mch_id, app_id, package_info, open_id } = response.data;
console.log("/wallet/withdraw:", response.data);
set_show_withdraw_popup(false);
// 调用微信商户转账接口
(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);
setInputValue("0.00");
// 重新加载数据
load_wallet_data();
},
fail: (res) => {
console.log("微信转账失败:", res);
},
});
}
const handlePasswordInput = (e: any) => {
const value = e.detail.value;
const [one = "", two = "", three = "", four = "", five = "", six = ""] = value.split("");
setPassword([one, two, three, four, five, six]);
if (value.length === 6) {
const timer = setTimeout(() => {
// TODO 校验密码
if (false) {
set_show_withdraw_popup(false);
Taro.showModal({
content: "支付密码错误,请重试",
cancelText: "忘记密码",
confirmText: "重试",
cancelColor: "#000",
confirmColor: "#fff",
}).then((res) => {
if (res.confirm) {
set_show_withdraw_popup(true);
} else if (res.cancel) {
Taro.navigateTo({
url: "/user_pages/validPhone/index"
});
}
}).finally(() => {
clearTimeout(timer);
});
} else {
submit_withdraw();
}
}, 100);
}
}
return (
<View className="withdrawal-page" >
<View className="withdrawal-container">
<Text className="title-text"></Text>
<View className="input-container">
<Text className="symbol">¥</Text>
<Input type="digit" placeholder="0.00" cursorColor="#000" value={inputValue} onInput={handleInput} />
{
!showTips && (Number(inputValue) !== 0) && (
<Button className="btn" onClick={handleWithdraw}></Button>
)
}
</View>
<View className="btn-container">
<View>
<Text>{`我的余额:¥${walletInfo.balance}`}</Text>
<Text>¥5000.00</Text>
</View>
<Text className="btn" onClick={withdrawAll}></Text>
</View>
{
showTips && (
<View className="tips-text">{tipsText}</View>
)
}
</View>
<View className="tips-container">
<View className="title-text"></View>
<View className="tips-text">
<Text>1. </Text>
<Text>2. </Text>
<Text>3. </Text>
<Text>4. </Text>
<Text>5. </Text>
<Text>6. </Text>
<Text>7. </Text>
</View>
</View>
<View className="tips-container">
<View className="title-text"></View>
<View className="tips-text">
<Text>1. </Text>
<Text>2. </Text>
<Text>3. </Text>
<Text>4. 使</Text>
</View>
</View>
{/* 提现输入密码弹窗 */}
<CommonPopup
visible={show_withdraw_popup}
onClose={() => set_show_withdraw_popup(false)}
title="提现"
className="withdraw_popup"
hideFooter={true}
>
<View className="popup_content">
<View className="popup_text">{`¥${inputValue}`}</View>
<View className="password_container">
{
password.map((item, index) => (
<View key={index} className="password_item">
<Text className="password_text">{item}</Text>
</View>
))
}
</View>
<Input focus={isFocus} type="number" style={{ width: "0", height: "0", opacity: "0" }} value={password.filter(item => item !== "").join("")} maxlength={6} onInput={handlePasswordInput} />
</View>
</CommonPopup>
</View >
);
};
export default Withdrawal;