diff --git a/src/app.config.ts b/src/app.config.ts index 2a7f0a5..5ae88f1 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -30,6 +30,9 @@ export default defineAppConfig({ "downloadBill/index", // 下载账单 "downloadBillRecords/index", // 下载账单记录 "billDetail/index", // 账单详情 + "setTransactionPassword/index", // 设置交易密码 + "validPhone/index", // 验证手机号 + "withdrawal/index", // 提现 ], }, // { diff --git a/src/components/Picker/PopupPicker.tsx b/src/components/Picker/PopupPicker.tsx index c1c58cb..75de996 100644 --- a/src/components/Picker/PopupPicker.tsx +++ b/src/components/Picker/PopupPicker.tsx @@ -17,7 +17,7 @@ interface PickerOption { interface PickerProps { visible: boolean; setvisible: (visible: boolean) => void; - options?: PickerOption[][]; + options?: PickerOption[][] | PickerOption[]; value?: (string | number)[]; type?: "month" | "day" | "hour" | "ntrp" | null; img?: string; diff --git a/src/config/api.ts b/src/config/api.ts index f54531e..489dd2d 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -29,7 +29,9 @@ export const API_CONFIG = { CREATE: '/game/create', JOIN: '/game/join', LEAVE: '/game/leave' - } + }, + PROFESSIONS: '/professions/tree', + CITIS: '/admin/wch_cities/page' }; // 请求拦截器配置 diff --git a/src/services/userService.ts b/src/services/userService.ts index 02fe672..3578eeb 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -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 移过来) export interface UserInfoType { 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 { // 数据转换函数:将后端数据转换为ListContainer期望的格式 @@ -206,7 +232,7 @@ export class UserService { date_str = `明天(${weekday})`; } else if (start_date.getTime() === day_after_tomorrow.getTime()) { 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; } else { 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) { const userData = response.data; return { - id: userData.id || '', + id: userData.id || '', nickname: userData.nickname || '', avatar: userData.avatar_url || '', 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 ''; } } + + // 获取职业树 + static async getProfessions(): Promise<[] | PickerOption[]> { + try { + const response = await httpService.post(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(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 移过来的用户相关方法 diff --git a/src/user_pages/billDetail/index.tsx b/src/user_pages/billDetail/index.tsx index fa68f99..9a755e9 100644 --- a/src/user_pages/billDetail/index.tsx +++ b/src/user_pages/billDetail/index.tsx @@ -1,6 +1,10 @@ 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 "./index.scss"; @@ -10,51 +14,84 @@ enum FreezeActions { } interface BillDetail { - id: number; - transaction_type: TransactionType; - transaction_sub_type: TransactionSubType; - freeze_action: FreezeActions; - amount: number; - description: string; - related_id: number; - create_time: string; - order_no: string; - game_title: string; - order_amount: number; - type_text: string; - sub_type_text: string; - amount_yuan: string; + id?: number; + transaction_type?: TransactionType; + transaction_sub_type?: TransactionSubType; + freeze_action?: FreezeActions; + amount?: number; + description?: string; + related_id?: number; + create_time?: string; + order_no?: string; + game_title?: string; + order_amount?: number; + type_text?: string; + sub_type_text?: string; + amount_yuan?: string; } const BillDetail: React.FC = () => { - const [billDetail, setBillDetail] = useState(null); + const router = useRouter(); + const { id } = router.params; + const [billDetail, setBillDetail] = useState({}); + + const getBillDetail = async () => { + try { + const res = await httpService.post("/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 ( 现金交易 (元) - + - 65.00 + {billDetail.transaction_type === 'expense' ? '-' : '+'} + {billDetail.amount_yuan} 交易时间 - 2025-02-16 12:21:54 + {billDetail.create_time && dayjs(billDetail.create_time).format('YYYY-MM-DD HH:mm:ss')} 活动标题 - 女生轻松双打 + {billDetail.game_title} 现金余额 - ¥3890.00 + ¥{billDetail.amount} - 交易单号 + 商户单号 - 89172371293791273912 - 复制 + {billDetail.order_no} + copyText(billDetail.order_no)}>复制 diff --git a/src/user_pages/downloadBill/index.scss b/src/user_pages/downloadBill/index.scss index 2a10ee3..e0682d9 100644 --- a/src/user_pages/downloadBill/index.scss +++ b/src/user_pages/downloadBill/index.scss @@ -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; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/user_pages/downloadBill/index.tsx b/src/user_pages/downloadBill/index.tsx index 0dd43fd..5632094 100644 --- a/src/user_pages/downloadBill/index.tsx +++ b/src/user_pages/downloadBill/index.tsx @@ -1,20 +1,47 @@ 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 dayjs from "dayjs"; import "./index.scss"; import { DialogCalendarCard } from "@/components/index"; // 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 { + label: string; + value: T; +} +interface TransactionLoadParams { + transaction_sub_type: TransactionSubType; + date_range?: string[]; +} const DownloadBill: React.FC = () => { const [dateRange, setDateRange] = useState({ start: "", end: "" }); + const [transactionSubType, setTransactionSubType] = + useState(TransactionSubType.All); const [dateType, setDateType] = useState("week"); const [visible, setVisible] = useState(false); + const [show_download_popup, set_show_download_popup] = useState(false); + const [isFocus, setIsFocus] = useState(false); + const [password, setPassword] = useState(new Array(6).fill("")); useEffect(() => { culculateDateRange(dateType); }, []); - + const [showFilterPopup, setShowFilterPopup] = useState(false); const culculateDateRange = (dateType: string) => { const today = new Date(); const year = today.getFullYear(); @@ -49,17 +76,18 @@ const DownloadBill: React.FC = () => { } switch (range) { case "week": + setCurrentTimeValue(new Date()); setDateType("week"); culculateDateRange("week"); break; case "month": + setCurrentTimeValue(new Date()); setDateType("month"); culculateDateRange("month"); break; case "custom": setDateType("custom"); setDateRange({ start: "", end: "" }); - setVisible(true); break; } }; @@ -74,23 +102,136 @@ const DownloadBill: React.FC = () => { 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[] = [ + { + 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({ + 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 ( 最长可导出三个月的账单 - 示例文件 + {/* 示例文件 */} - + {/* 接收方式 小程序消息 - - + */} + { + setShowFilterPopup(true); + }} + > 交易类型 - 全部 + + { + transaction_type_options.find( + (item) => + item.value === load_transactions_params.transaction_sub_type + )?.label + } + @@ -105,9 +246,8 @@ const DownloadBill: React.FC = () => { 近一周 { selectDateRange("month"); }} @@ -115,9 +255,8 @@ const DownloadBill: React.FC = () => { 近一月 { selectDateRange("custom"); }} @@ -126,11 +265,28 @@ const DownloadBill: React.FC = () => { - {dateRange.start && dateRange.end && ( + {dateRange.start && dateRange.end && dateType !== "custom" && ( {dateRange.start}{dateRange.end} )} + {dateType === "custom" && ( + { + setVisible(true); + }} + > + 时间范围 + + + {dateRange.start && dateRange.end + ? `${dateRange.start} 至 ${dateRange.end}` + : "请选择账单时间"} + + + + )} { > 下载记录 - + {visible && ( { onClose={() => setVisible(false)} /> )} + + {/* 下载账单输入密码弹窗 */} + set_show_download_popup(false)} + title="提现" + className="withdraw_popup" + hideFooter={true} + > + + {`支付账单流水文件(文件名).xlsx`} + {`文件大小:7KB`} + {`请输入交易密码`} + + {password.map((item, index) => ( + + {item} + + ))} + + item !== "").join("")} + maxlength={6} + onInput={handlePasswordInput} + /> + + + + {/* 筛选账单弹窗 */} + + + + + 交易类型 + + {transaction_type_options.map( + (option: Option) => ( + { + setTransactionSubType(option.value); + }} + > + {option.label} + + ) + )} + + + + + ); }; diff --git a/src/user_pages/downloadBillRecords/index.scss b/src/user_pages/downloadBillRecords/index.scss index e69de29..c92820d 100644 --- a/src/user_pages/downloadBillRecords/index.scss +++ b/src/user_pages/downloadBillRecords/index.scss @@ -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); + } +} \ No newline at end of file diff --git a/src/user_pages/downloadBillRecords/index.tsx b/src/user_pages/downloadBillRecords/index.tsx index c45a50d..fe7808f 100644 --- a/src/user_pages/downloadBillRecords/index.tsx +++ b/src/user_pages/downloadBillRecords/index.tsx @@ -1,9 +1,75 @@ 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 [records, setRecords] = useState([]); + 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 ( - 下载记录 + + + { + records.map((record) => ( + + {record.file_name} + + 申请时间 + {record.create_time} + + + 账单范围 + {record.date_range_desc} + + + + 查看材料 + + + )) + } + + 出于信息安全考虑,仅保留并展示7天内的账单下载记录 + ); }; diff --git a/src/user_pages/edit/index.tsx b/src/user_pages/edit/index.tsx index 8d54365..a8c2819 100644 --- a/src/user_pages/edit/index.tsx +++ b/src/user_pages/edit/index.tsx @@ -1,50 +1,50 @@ -import React, { useState, useEffect } from 'react'; -import { View, Text, Image, ScrollView, Button } from '@tarojs/components'; -import { PopupPicker } from '@/components/Picker/index' -import Taro from '@tarojs/taro'; -import './index.scss'; -import { UserInfo } from '@/components/UserInfo'; -import { UserService } from '@/services/userService'; -import { clear_login_state } from '@/services/loginService'; -import { convert_db_gender_to_display } from '@/utils/genderUtils'; -import { EditModal } from '@/components'; +import React, { useState, useEffect } from "react"; +import { View, Text, Image, ScrollView, Button } from "@tarojs/components"; +import { PopupPicker } from "@/components/Picker/index"; +import Taro from "@tarojs/taro"; +import "./index.scss"; +import { UserInfo } from "@/components/UserInfo"; +import { UserService, PickerOption } from "@/services/userService"; +import { clear_login_state } from "@/services/loginService"; +import { convert_db_gender_to_display } from "@/utils/genderUtils"; +import { EditModal } from "@/components"; import img from "@/config/images"; const EditProfilePage: React.FC = () => { // 用户信息状态 const [user_info, setUserInfo] = useState({ - id: '1', - nickname: '加载中...', - avatar: require('@/static/userInfo/default_avatar.svg'), - join_date: '加载中...', + id: "1", + nickname: "加载中...", + avatar: require("@/static/userInfo/default_avatar.svg"), + join_date: "加载中...", stats: { following: 0, friends: 0, hosted: 0, - participated: 0 + participated: 0, }, - personal_profile: '加载中...', - occupation: '加载中...', - ntrp_level: 'NTRP 3.0', - phone: '', - gender: '', - country: '', - province: '', - city: '', + personal_profile: "加载中...", + occupation: "加载中...", + ntrp_level: "NTRP 3.0", + phone: "", + gender: "", + country: "", + province: "", + city: "", }); // 表单状态 const [form_data, setFormData] = useState({ - nickname: '', - personal_profile: '', - occupation: '', - ntrp_level: '4.0', - phone: '', - gender: '', - birthday: '2000-01-01', - country: '', - province: '', - city: '' + nickname: "", + personal_profile: "", + occupation: "", + ntrp_level: "4.0", + phone: "", + gender: "", + birthday: "2000-01-01", + country: "", + province: "", + city: "", }); // 加载状态 @@ -52,18 +52,44 @@ const EditProfilePage: React.FC = () => { // 编辑弹窗状态 const [edit_modal_visible, setEditModalVisible] = useState(false); - const [editing_field, setEditingField] = useState(''); + const [editing_field, setEditingField] = useState(""); const [gender_picker_visible, setGenderPickerVisible] = useState(false); const [birthday_picker_visible, setBirthdayPickerVisible] = useState(false); const [location_picker_visible, setLocationPickerVisible] = 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([]); + + // 城市数据 + const [cities, setCities] = useState([]); // 页面加载时初始化数据 useEffect(() => { 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 () => { try { @@ -71,23 +97,23 @@ const EditProfilePage: React.FC = () => { const user_data = await UserService.get_user_info(); setUserInfo(user_data); setFormData({ - nickname: user_data.nickname || '', - personal_profile: user_data.personal_profile || '', - occupation: user_data.occupation || '', - ntrp_level: user_data.ntrp_level || 'NTRP 4.0', - phone: user_data.phone || '', - gender: user_data.gender || '', - birthday: user_data.birthday || '', - country: user_data.country || '', - province: user_data.province || '', - city: user_data.city || '' + nickname: user_data.nickname || "", + personal_profile: user_data.personal_profile || "", + occupation: user_data.occupation || "", + ntrp_level: user_data.ntrp_level || "NTRP 4.0", + phone: user_data.phone || "", + gender: user_data.gender || "", + birthday: user_data.birthday || "", + country: user_data.country || "", + province: user_data.province || "", + city: user_data.city || "", }); } catch (error) { - console.error('加载用户信息失败:', error); + console.error("加载用户信息失败:", error); Taro.showToast({ - title: '加载用户信息失败', - icon: 'error', - duration: 2000 + title: "加载用户信息失败", + icon: "error", + duration: 2000, }); } finally { setLoading(false); @@ -98,51 +124,51 @@ const EditProfilePage: React.FC = () => { const handle_avatar_upload = () => { Taro.chooseImage({ count: 1, - sizeType: ['compressed'], - sourceType: ['album', 'camera'], + sizeType: ["compressed"], + sourceType: ["album", "camera"], success: async (res) => { const tempFilePath = res.tempFilePaths[0]; try { const avatar_url = await UserService.upload_avatar(tempFilePath); - setUserInfo(prev => ({ ...prev, avatar: avatar_url })); + setUserInfo((prev) => ({ ...prev, avatar: avatar_url })); Taro.showToast({ - title: '头像上传成功', - icon: 'success' + title: "头像上传成功", + icon: "success", }); } catch (error) { - console.error('头像上传失败:', error); + console.error("头像上传失败:", error); Taro.showToast({ - title: '头像上传失败', - icon: 'none' + title: "头像上传失败", + icon: "none", }); } - } + }, }); }; // 处理编辑弹窗 const handle_open_edit_modal = (field: string) => { - if (field === 'gender') { + if (field === "gender") { setGenderPickerVisible(true); return; } - if (field === 'birthday') { + if (field === "birthday") { setBirthdayPickerVisible(true); return; } - if (field === 'location') { + if (field === "location") { setLocationPickerVisible(true); return; } - if (field === 'ntrp_level') { + if (field === "ntrp_level") { setNtrpPickerVisible(true); return; } - if (field === 'occupation') { + if (field === "occupation") { setOccupationPickerVisible(true); return; } - if (field === 'nickname') { + if (field === "nickname") { // 手动输入 setEditingField(field); setEditModalVisible(true); @@ -159,60 +185,67 @@ const EditProfilePage: React.FC = () => { await UserService.update_user_info(update_data); // 更新本地状态 - setFormData(prev => ({ ...prev, [editing_field]: value })); - setUserInfo(prev => ({ ...prev, [editing_field]: value })); + setFormData((prev) => ({ ...prev, [editing_field]: value })); + setUserInfo((prev) => ({ ...prev, [editing_field]: value })); // 关闭弹窗 setEditModalVisible(false); - setEditingField(''); + setEditingField(""); // 显示成功提示 Taro.showToast({ - title: '保存成功', - icon: 'success' + title: "保存成功", + icon: "success", }); } catch (error) { - console.error('保存失败:', error); + console.error("保存失败:", error); Taro.showToast({ - title: '保存失败', - icon: 'error' + title: "保存失败", + icon: "error", }); } }; const handle_edit_modal_cancel = () => { 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 { - if (typeof field === 'object' && field !== null && !Array.isArray(field)) { + if ( + typeof field === "object" && + field !== null && + !Array.isArray(field) + ) { await UserService.update_user_info({ ...field }); // 更新本地状态 - setFormData(prev => ({ ...prev, ...field })); - setUserInfo(prev => ({ ...prev, ...field })); + setFormData((prev) => ({ ...prev, ...field })); + setUserInfo((prev) => ({ ...prev, ...field })); } else { // 调用更新用户信息接口,只传递修改的字段 const update_data = { [field as string]: value }; await UserService.update_user_info(update_data); // 更新本地状态 - setFormData(prev => ({ ...prev, [field as string]: value })); - setUserInfo(prev => ({ ...prev, [field as string]: value })); + setFormData((prev) => ({ ...prev, [field as string]: value })); + setUserInfo((prev) => ({ ...prev, [field as string]: value })); } // 显示成功提示 Taro.showToast({ - title: '保存成功', - icon: 'success' + title: "保存成功", + icon: "success", }); } catch (error) { - console.error('保存失败:', error); + console.error("保存失败:", error); Taro.showToast({ - title: '保存失败', - icon: 'error' + title: "保存失败", + icon: "error", }); } }; @@ -220,13 +253,19 @@ const EditProfilePage: React.FC = () => { // 处理性别选择 const handle_gender_change = (e: any) => { const gender_value = e[0]; - handle_field_edit('gender', gender_value); + handle_field_edit("gender", gender_value); }; // 处理生日选择 const handle_birthday_change = (e: any) => { 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水平选择 const handle_ntrp_level_change = (e: any) => { 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 [country, province] = e; - handle_field_edit('occupation', `${country} ${province}`); + handle_field_edit("occupation", `${country} ${province}`); }; // 处理退出登录 const handle_logout = () => { Taro.showModal({ - title: '确认退出', - content: '确定要退出登录吗?', + title: "确认退出", + content: "确定要退出登录吗?", success: (res) => { if (res.confirm) { // 清除用户数据 clear_login_state(); Taro.reLaunch({ - url: '/login_pages/index/index' + url: "/login_pages/index/index", }); } - } + }, }); }; const onGetPhoneNumber = async (e) => { if (!e.detail || !e.detail.code) { Taro.showToast({ - title: '获取手机号失败,请重试', - icon: 'none', - duration: 2000 + title: "获取手机号失败,请重试", + icon: "none", + duration: 2000, }); return; } try { const phone = await UserService.parse_phone(e.detail.code); - handle_field_edit('phone', phone); + handle_field_edit("phone", phone); } catch (e) { - console.error('解析手机号失败:', e); + console.error("解析手机号失败:", e); Taro.showToast({ - title: '解析手机号失败,请重试', - icon: 'none', - duration: 2000 + title: "解析手机号失败,请重试", + icon: "none", + duration: 2000, }); } - } + }; return ( {/* 导航栏 */} - { Taro.navigateBack() }}> + { + Taro.navigateBack(); + }} + > { - - {/* 基本信息编辑 */} {/* 名字 */} - handle_open_edit_modal('nickname')}> + handle_open_edit_modal("nickname")} + > - + 名字 - {form_data.nickname || '188的王晨'} - + + {form_data.nickname || "188的王晨"} + + @@ -342,14 +395,25 @@ const EditProfilePage: React.FC = () => { {/* 性别 */} - handle_open_edit_modal('gender')}> + handle_open_edit_modal("gender")} + > - + 性别 - {convert_db_gender_to_display(form_data.gender)} - + + {convert_db_gender_to_display(form_data.gender)} + + @@ -357,14 +421,23 @@ const EditProfilePage: React.FC = () => { {/* 生日 */} - handle_open_edit_modal('birthday')}> + handle_open_edit_modal("birthday")} + > - + 生日 {form_data.birthday} - + @@ -373,16 +446,26 @@ const EditProfilePage: React.FC = () => { {/* 简介编辑 */} - handle_open_edit_modal('personal_profile')}> + handle_open_edit_modal("personal_profile")} + > - + 简介 - {form_data.personal_profile.replace(/\n/g, ' ') || '介绍一下自己'} + {form_data.personal_profile.replace(/\n/g, " ") || + "介绍一下自己"} - + @@ -392,40 +475,67 @@ const EditProfilePage: React.FC = () => { {/* 地区 */} - handle_open_edit_modal('location')}> + handle_open_edit_modal("location")} + > - + 地区 {`${form_data.country} ${form_data.province} ${form_data.city}`} - + {/* NTRP水平 */} - handle_open_edit_modal('ntrp_level')}> + handle_open_edit_modal("ntrp_level")} + > - + NTRP 水平 {form_data.ntrp_level} - + {/* 职业 */} - handle_open_edit_modal('occupation')}> + handle_open_edit_modal("occupation")} + > - + 职业 {form_data.occupation} - + @@ -436,7 +546,10 @@ const EditProfilePage: React.FC = () => { - + 手机 @@ -448,8 +561,17 @@ const EditProfilePage: React.FC = () => { onInput={handle_phone_input} onBlur={handle_phone_blur} /> */} - - + + @@ -477,63 +599,94 @@ const EditProfilePage: React.FC = () => { {/* 性别选择弹窗 */} - {gender_picker_visible && } + {gender_picker_visible && ( + + )} {/* 生日选择弹窗 */} - {birthday_picker_visible && } + {birthday_picker_visible && ( + + )} {/* 地区选择弹窗 */} - {location_picker_visible && } + {location_picker_visible && ( + + )} {/* NTRP水平选择弹窗 */} - {ntrp_picker_visible && } + {ntrp_picker_visible && ( + + )} {/* 职业选择弹窗 */} - {occupation_picker_visible && } + {occupation_picker_visible && ( + + )} ); }; diff --git a/src/user_pages/queryTransactions/index.scss b/src/user_pages/queryTransactions/index.scss index 7ea9960..d0bbd4e 100644 --- a/src/user_pages/queryTransactions/index.scss +++ b/src/user_pages/queryTransactions/index.scss @@ -1,197 +1,206 @@ .listSearchContainer { - padding: 0 15px; - padding-top: 16px; + padding: 0 15px; + padding-top: 16px; - .icon16 { - width: 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); + .icon16 { + width: 16px; + height: 16px; } - .searchText { - color: #000000; - font-size: 16px; - font-weight: 600; - line-height: 20px; - } - } + .topSearch { + padding: 5px 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); - .searchIcon { - width: 20px; - height: 20px; - } - - .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; + .nut-input { + padding: 0; + height: 100%; + } } - .historySearchClear { - 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 { + .searchRight { 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: 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; + .searchLine { + width: 1px; + height: 20px; + border-radius: 20px; + background: rgba(0, 0, 0, 0.06); } - .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; - } + .searchText { + color: #000000; + font-size: 16px; + font-weight: 600; + line-height: 20px; } - } + } - .transaction_right { + .searchIcon { + width: 20px; + height: 20px; + } + + .historySearchTitleWrapper { display: flex; - flex-direction: column; + padding: 12px 0; + justify-content: space-between; align-items: flex-end; - gap: 4px; - width: 68px; + align-self: stretch; - .transaction_amount { - font-size: 12px; - font-weight: 600; - color: #000; - line-height: 1.5; - text-align: right; + .historySearchTitle, + .historySearchClear { + color: #000; + font-size: 14px; + font-weight: 600; + line-height: 20px; } - .balance_info { - font-size: 10px; - font-weight: 400; - color: rgba(60, 60, 67, 0.6); - line-height: 1.2; - text-align: right; + .historySearchClear { + color: #9a9a9a; + display: flex; + align-items: center; + gap: 4px; } - } } - } -} + + .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; + } +} \ No newline at end of file diff --git a/src/user_pages/queryTransactions/index.tsx b/src/user_pages/queryTransactions/index.tsx index c86aafa..71bc6d2 100644 --- a/src/user_pages/queryTransactions/index.tsx +++ b/src/user_pages/queryTransactions/index.tsx @@ -341,6 +341,11 @@ const QueryTransactions = () => { )} + { + transactions.length > 0 && ( + 仅支持查找2024年9月1日以后的账单 + ) + } ); diff --git a/src/user_pages/setTransactionPassword/index.config.ts b/src/user_pages/setTransactionPassword/index.config.ts new file mode 100644 index 0000000..5f2a473 --- /dev/null +++ b/src/user_pages/setTransactionPassword/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '设置交易密码', +}) diff --git a/src/user_pages/setTransactionPassword/index.scss b/src/user_pages/setTransactionPassword/index.scss new file mode 100644 index 0000000..1e596ec --- /dev/null +++ b/src/user_pages/setTransactionPassword/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/user_pages/setTransactionPassword/index.tsx b/src/user_pages/setTransactionPassword/index.tsx new file mode 100644 index 0000000..15656f0 --- /dev/null +++ b/src/user_pages/setTransactionPassword/index.tsx @@ -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({ + 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 ( + + { + // handleType === "reset" && ( + // + // 旧密码 + // { handleInput(e, "old_password") }}> + // + // ) + } + + 交易密码 + { handleInput(e, "new_password") }}> + + + 重复密码 + { handleInput(e, "confirm_password") }}> + + { + handleType === "set" && ( + + 手机验证 + { handleInput(e, "sms_code") }}> + + + ) + } + * 密码由6位数字组成 + + + ); +}; + +export default SetTransactionPassword; diff --git a/src/user_pages/validPhone/index.config.ts b/src/user_pages/validPhone/index.config.ts new file mode 100644 index 0000000..6d6c0ff --- /dev/null +++ b/src/user_pages/validPhone/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '验证手机号', +}) diff --git a/src/user_pages/validPhone/index.scss b/src/user_pages/validPhone/index.scss new file mode 100644 index 0000000..e231107 --- /dev/null +++ b/src/user_pages/validPhone/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/user_pages/validPhone/index.tsx b/src/user_pages/validPhone/index.tsx new file mode 100644 index 0000000..4f8f92b --- /dev/null +++ b/src/user_pages/validPhone/index.tsx @@ -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({ + 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 ( + + + 手机号 + + + + 验证码 + { handleInput(e, "sms_code") }}> + + + + + ); +}; + +export default ValidPhone; diff --git a/src/user_pages/wallet/index.scss b/src/user_pages/wallet/index.scss index 51aad1b..03268b6 100644 --- a/src/user_pages/wallet/index.scss +++ b/src/user_pages/wallet/index.scss @@ -146,7 +146,6 @@ 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; @@ -154,6 +153,9 @@ align-items: center; padding: 12px 20px; border-bottom: 0.5px solid rgba(120, 120, 128, 0.12); + position: sticky; + top: 0; + background-color: #fff; .history_title { font-size: 16px; diff --git a/src/user_pages/wallet/index.tsx b/src/user_pages/wallet/index.tsx index e08db38..f6aa600 100644 --- a/src/user_pages/wallet/index.tsx +++ b/src/user_pages/wallet/index.tsx @@ -5,6 +5,7 @@ import "./index.scss"; import { CommonPopup } from "@/components"; import httpService from "@/services/httpService"; import { withAuth } from "@/components"; +import { PopupPicker } from "@/components/Picker/index"; // 交易记录类型 interface Transaction { @@ -103,6 +104,7 @@ const WalletPage: React.FC = () => { const [show_withdraw_popup, set_show_withdraw_popup] = useState(false); const [withdraw_amount, set_withdraw_amount] = useState(""); const [submitting, set_submitting] = useState(false); + const [password_status, set_password_status] = useState(false); // 交易记录状态 const [transactions, set_transactions] = useState([]); @@ -110,6 +112,16 @@ const WalletPage: React.FC = () => { // 交易记录过滤状态 const [showFilterPopup, setShowFilterPopup] = useState(false); + const [showMonthPicker, setShowMonthPicker] = useState(false); + + const [filterParams, setFilterParams] = useState({ + type: TransactionType.All, + transaction_sub_type: TransactionSubType.All, + }); + + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); const [load_transactions_params, set_load_transactions_params] = useState({ @@ -118,15 +130,40 @@ const WalletPage: React.FC = () => { type: TransactionType.All, transaction_sub_type: TransactionSubType.All, keyword: "", - date: "", + date: `${year}-${month}` }); + useEffect(() => { + load_transactions(); + }, [load_transactions_params]); // 页面显示时加载数据 useDidShow(() => { 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 () => { try { @@ -171,11 +208,10 @@ const WalletPage: React.FC = () => { // 加载交易记录 const load_transactions = async () => { setShowFilterPopup(false); - set_load_transactions_params({ ...load_transactions_params, page: 1 }); + // set_load_transactions_params({ ...load_transactions_params, page: 1 }); try { set_loading_transactions(true); console.log("开始加载交易记录..."); - const response = await httpService.post("/wallet/transactions", { ...load_transactions_params, }); @@ -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 = () => { + if (password_status) { + navigateToSetTransactionPassword("set"); + return; + } if (wallet_info.balance <= 0) { Taro.showToast({ title: "余额不足", @@ -226,7 +278,10 @@ const WalletPage: React.FC = () => { }); 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) => { + time = time.replace(/-/g, "/"); const date = new Date(time); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); @@ -350,6 +406,14 @@ const WalletPage: React.FC = () => { setShowFilterPopup(true); }; + const handleFilterCancel = () => { + setShowFilterPopup(false); + setFilterParams({ + type: load_transactions_params.type, + transaction_sub_type: load_transactions_params.transaction_sub_type, + }); + }; + return ( {/* 钱包主卡片 */} @@ -357,7 +421,7 @@ const WalletPage: React.FC = () => { {/* 头部信息 */} 我的现金 - 修改交易密码 + navigateToSetTransactionPassword("reset")}>修改交易密码 {/* 余额显示 */} @@ -420,7 +484,8 @@ const WalletPage: React.FC = () => { /> 下载账单 - + {/* TODO 客服中心 */} + Taro.navigateTo({ url: "/user_pages/validPhone/index" })}> { {/* 标题栏 */} 现金明细 - - 2025-09 + setShowMonthPicker(true)}> + {load_transactions_params.date} @@ -526,12 +591,30 @@ const WalletPage: React.FC = () => { - + {/* 选择月份弹窗 */} + {showMonthPicker && ( + { + const [year, month] = e; + set_load_transactions_params({ + ...load_transactions_params, + date: `${year}-${String(month).padStart(2, "0")}`, + }); + }} + /> + )} {/* 筛选账单弹窗 */} setShowFilterPopup(false)} - onConfirm={load_transactions} + onClose={handleFilterCancel} + onConfirm={modify_load_transactions_params} title="选择筛选项" className="filter_popup" > @@ -544,14 +627,14 @@ const WalletPage: React.FC = () => { (option: Option) => ( { - set_load_transactions_params({ - ...load_transactions_params, + setFilterParams({ + ...filterParams, type: option.value, }); }} @@ -569,15 +652,15 @@ const WalletPage: React.FC = () => { (option: Option) => ( { - set_load_transactions_params({ - ...load_transactions_params, + setFilterParams({ + ...filterParams, transaction_sub_type: option.value, }); }} diff --git a/src/user_pages/withdrawal/index.config.ts b/src/user_pages/withdrawal/index.config.ts new file mode 100644 index 0000000..6c10df6 --- /dev/null +++ b/src/user_pages/withdrawal/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '钱包', +}) diff --git a/src/user_pages/withdrawal/index.scss b/src/user_pages/withdrawal/index.scss new file mode 100644 index 0000000..c9ab7da --- /dev/null +++ b/src/user_pages/withdrawal/index.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/user_pages/withdrawal/index.tsx b/src/user_pages/withdrawal/index.tsx new file mode 100644 index 0000000..114430a --- /dev/null +++ b/src/user_pages/withdrawal/index.tsx @@ -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(""); + const [inputValue, setInputValue] = useState("0.00"); + const [walletInfo, setWalletInfo] = useState({ + balance: "0.00", + }); + const [isFocus, setIsFocus] = useState(false); + + const [show_withdraw_popup, set_show_withdraw_popup] = useState(false); + + const [password, setPassword] = useState(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 ( + + + 提现金额 + + ¥ + + { + !showTips && (Number(inputValue) !== 0) && ( + + ) + } + + + + {`我的余额:¥${walletInfo.balance}`} + (可提现余额:¥5000.00) + + 全部提现 + + { + showTips && ( + {tipsText} + ) + } + + + 提现须知 + + 1. 本钱包余额由平台统一管理,提现服务由 微信支付(财付通) 提供。 + 2. 提现仅支持提现至本人实名认证的微信支付账户,请确保账户信息真实有效。 + 3. 每次提现金额不得超过钱包余额,且需满足提现金额最低限制(如有)。 + 4. 正常情况下提现 实时到账,如遇网络、系统或银行通道原因,可能会有延迟。 + 5. 提现过程中平台不收取任何手续费,如微信支付有特殊规则,按其实际规定执行。 + 6. 若发现异常交易、涉嫌违规或存在风险,平台有权暂停或拒绝提现操作。 + 7. 实际到账结果以 微信支付通知 为准。 + + + + + 免费声明 + + 1. 本平台不向用户收取提现手续费,提现服务免费。 + 2. 若因银行或微信支付方收取相关费用,与本平台无关。 + 3. 因网络、系统或第三方原因造成的延迟、失败,本平台不承担责任。 + 4. 用户在使用提现服务前,应充分了解并同意上述规则。 + + + + {/* 提现输入密码弹窗 */} + set_show_withdraw_popup(false)} + title="提现" + className="withdraw_popup" + hideFooter={true} + > + + {`¥${inputValue}`} + + { + password.map((item, index) => ( + + {item} + + )) + } + + item !== "").join("")} maxlength={6} onInput={handlePasswordInput} /> + + + + ); +}; + +export default Withdrawal; \ No newline at end of file