feat(user): 实现扫码登录和推荐人绑定功能- 添加扫码登录相关API接口,包括生成二维码、检查状态、确认登录等

- 在用户注册时支持从邀请参数中获取推荐人ID并绑定
- 修改管理员面板UI,添加统一扫码功能入口- 更新用户管理相关API地址,统一使用SERVER_API_URL
- 调整优惠券卡片样式,移除小程序不支持的CSS属性
- 添加聊天会话和消息管理相关API模块
- 新增分销商银行卡管理API接口
- 修改系统用户模型,增加推荐人ID字段
- 更新广告位查询接口,支持根据code获取广告位
- 调整邀请绑定接口参数,将refereeId改为dealerId
- 修改环境配置中的应用名称为"时里院子市集"
- 移除分享到朋友圈的相关代码
- 添加管理员面板组件,提供统一扫码等管理功能
-修复用户管理API请求参数传递问题
- 添加聊天消息和会话管理的完整CRUD接口
- 更新系统用户相关接口URL,确保正确调用后端服务
- 添加分销商银行卡管理的完整API接口实现
- 修改邀请绑定接口,使用dealerId替代refereeId参数
- 修复扫码登录相关API的URL拼接问题
- 添加二维码内容解析功能,支持多种格式的token提取
- 更新用户信息模型,增加推荐人ID字段
-优化管理员面板样式和交互逻辑- 调整优惠券组件样式,兼容小程序环境限制- 修复用户管理模块的API调用问题
- 添加聊天相关数据模型和接口定义
- 更新环境配置中的应用名称
-修复邀请绑定相关的参数传递问题- 添加扫码登录状态枚举和相关数据结构定义- 优化管理员功能面板的UI展示和交互体验- 修复系统用户管理接口的请求参数问题
- 添加分销商银行卡管理相关接口实现- 调整聊天消息和会话管理接口的数据结构定义
-修复用户管理模块中的API调用路径问题
- 添加扫码登录相关工具函数,如设备信息获取等
- 更新邀请绑定接口的数据模型定义
-优化管理员面板组件的样式和功能实现
-修复系统用户管理接口中的参数传递问题
- 添加聊天相关模块的完整API接口实现
- 调整分销商银行卡管理模块的数据结构定义- 修复扫码登录相关接口的URL拼接问题- 更新用户管理模块中的API调用方式
- 添加聊天消息批量发送等相关接口实现- 修复邀请绑定接口中的参数名称问题- 优化管理员面板组件的功能和交互逻辑
- 调整系统用户管理接口的请求参数传递方式
- 添加分销商银行卡管理模块的完整接口实现
-修复聊天相关接口中的数据结构问题
- 更新扫码登录相关接口的数据模型定义
- 优化管理员功能面板的展示效果和用户体验
This commit is contained in:
2025-09-24 19:23:56 +08:00
parent 3d5b77ae51
commit 87d6648989
131 changed files with 11406 additions and 1446 deletions

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '提现申请'
})

View File

@@ -0,0 +1,424 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import {
Cell,
Button,
Form,
Input,
CellGroup,
Radio,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import { Wallet } from '@nutui/icons-react-taro'
import { businessGradients } from '@/styles/gradients'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
const { dealerUser } = useDealerUser()
// 获取可提现余额
const fetchBalance = useCallback(async () => {
try {
setAvailableAmount(dealerUser?.money || '0.00')
} catch (error) {
console.error('获取余额失败:', error)
}
}, [])
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const result = await pageShopDealerWithdraw({
page: 1,
limit: 100,
userId: dealerUser.userId
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record,
accountDisplay: getAccountDisplay(record)
}))
setWithdrawRecords(processedRecords)
}
} catch (error) {
console.error('获取提现记录失败:', error)
Taro.showToast({
title: '获取提现记录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 格式化账户显示
const getAccountDisplay = (record: ShopDealerWithdraw) => {
if (record.payType === 10) {
return '微信钱包'
} else if (record.payType === 20 && record.alipayAccount) {
return `支付宝(${record.alipayAccount.slice(-4)})`
} else if (record.payType === 30 && record.bankCard) {
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
}
return '未知账户'
}
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
setRefreshing(false)
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchBalance().then()
fetchWithdrawRecords().then()
}
}, [fetchBalance, fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 40: return '已到账'
case 20: return '审核通过'
case 10: return '待审核'
case 30: return '已驳回'
default: return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 40: return 'success'
case 20: return 'success'
case 10: return 'warning'
case 30: return 'danger'
default: return 'default'
}
}
const handleSubmit = async (values: any) => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息获取失败',
icon: 'error'
})
return
}
// 验证提现金额
const amount = parseFloat(values.amount)
const available = parseFloat(availableAmount.replace(',', ''))
if (amount < 100) {
Taro.showToast({
title: '最低提现金额为100元',
icon: 'error'
})
return
}
if (amount > available) {
Taro.showToast({
title: '提现金额超过可用余额',
icon: 'error'
})
return
}
try {
setSubmitting(true)
const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId,
money: values.amount,
payType: values.accountType === 'wechat' ? 10 :
values.accountType === 'alipay' ? 20 : 30,
applyStatus: 10, // 待审核
platform: 'MiniProgram'
}
// 根据提现方式设置账户信息
if (values.accountType === 'alipay') {
withdrawData.alipayAccount = values.account
withdrawData.alipayName = values.accountName
} else if (values.accountType === 'bank') {
withdrawData.bankCard = values.account
withdrawData.bankAccount = values.accountName
withdrawData.bankName = values.bankName || '银行卡'
}
await addShopDealerWithdraw(withdrawData)
Taro.showToast({
title: '提现申请已提交',
icon: 'success'
})
// 重置表单
formRef.current?.resetFields()
setSelectedAccount('')
// 刷新数据
await handleRefresh()
// 切换到提现记录页面
setActiveTab('1')
} catch (error: any) {
console.error('提现申请失败:', error)
Taro.showToast({
title: error.message || '提现申请失败',
icon: 'error'
})
} finally {
setSubmitting(false)
}
}
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({ amount })
}
const setAllAmount = () => {
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
}
const renderWithdrawForm = () => (
<View className="p-4">
{/* 余额卡片 */}
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 - 小程序兼容版本 */}
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
right: '-12px',
top: '-12px'
}}></View>
<View className="flex items-center justify-between relative z-10">
<View>
<Text className="text-white text-opacity-80 text-sm mb-1"></Text>
<Text className="text-2xl font-bold text-white">¥{availableAmount}</Text>
</View>
<View className="p-3 rounded-full" style={{
background: 'rgba(255, 255, 255, 0.2)'
}}>
<Wallet color="white" size="32" />
</View>
</View>
<View className="mt-4 pt-4 relative z-10" style={{
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
<Text className="text-white text-opacity-80 text-xs">
¥100 |
</Text>
</View>
</View>
<Form
ref={formRef}
onFinish={handleSubmit}
labelPosition="top"
>
<CellGroup>
<Form.Item name="amount" label="提现金额" required>
<Input
placeholder="请输入提现金额"
type="number"
clearable
/>
</Form.Item>
{/* 快捷金额 */}
<View className="px-4 py-2">
<Text className="text-sm text-gray-600 mb-2"></Text>
<View className="flex flex-wrap gap-2">
{quickAmounts.map(amount => (
<Button
key={amount}
size="small"
fill="outline"
onClick={() => setQuickAmount(amount)}
>
{amount}
</Button>
))}
<Button
size="small"
fill="outline"
onClick={setAllAmount}
>
</Button>
</View>
</View>
<Form.Item name="accountType" label="提现方式" required>
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
<Cell.Group>
<Cell>
<Radio value="wechat"></Radio>
</Cell>
<Cell>
<Radio value="alipay"></Radio>
</Cell>
<Cell>
<Radio value="bank"></Radio>
</Cell>
</Cell.Group>
</Radio.Group>
</Form.Item>
{selectedAccount === 'alipay' && (
<>
<Form.Item name="account" label="支付宝账号" required>
<Input placeholder="请输入支付宝账号" />
</Form.Item>
<Form.Item name="accountName" label="支付宝姓名" required>
<Input placeholder="请输入支付宝实名姓名" />
</Form.Item>
</>
)}
{selectedAccount === 'bank' && (
<>
<Form.Item name="bankName" label="开户银行" required>
<Input placeholder="请输入开户银行名称" />
</Form.Item>
<Form.Item name="account" label="银行卡号" required>
<Input placeholder="请输入银行卡号" />
</Form.Item>
<Form.Item name="accountName" label="开户姓名" required>
<Input placeholder="请输入银行卡开户姓名" />
</Form.Item>
</>
)}
{selectedAccount === 'wechat' && (
<View className="px-4 py-2">
<Text className="text-sm text-gray-500">
</Text>
</View>
)}
</CellGroup>
<View className="mt-6 px-4">
<Button
block
type="primary"
nativeType="submit"
loading={submitting}
disabled={submitting || !selectedAccount}
>
{submitting ? '提交中...' : '申请提现'}
</Button>
</View>
</Form>
</View>
)
const renderWithdrawRecords = () => (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</View>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
))
) : (
<Empty description="暂无提现记录" />
)}
</View>
</PullToRefresh>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs.TabPane title="申请提现" value="0">
{renderWithdrawForm()}
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
</View>
)
}
export default DealerWithdraw