forked from gxwebsoft/mp-10550
- 配置文件中更新测试环境API基础URL - 添加ShopDealerWithdrawCreateResult类型定义以支持微信转账返回的package_info - 修改addShopDealerWithdraw函数以处理微信转账流程的特殊返回值 - 实现extractPackageInfo、canRequestMerchantTransferConfirm和requestMerchantTransferConfirm辅助函数 - 在微信钱包提现流程中集成商户转账确认页面调用 - 添加对微信小程序环境的检测和错误处理 - 更新快速金额选项,增加1元选项 - 修改微信提现提示文字,说明需要确认收款的流程
599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||
import {View, Text} from '@tarojs/components'
|
||
import {
|
||
Cell,
|
||
Space,
|
||
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 extractPackageInfo = (result: unknown): string | null => {
|
||
if (!result || typeof result !== 'object') return null
|
||
const r = result as any
|
||
return (
|
||
r.package_info ??
|
||
r.packageInfo ??
|
||
r.package ??
|
||
null
|
||
)
|
||
}
|
||
|
||
const canRequestMerchantTransferConfirm = (): boolean => {
|
||
try {
|
||
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
||
const env = (Taro as any).getEnv()
|
||
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
||
}
|
||
|
||
const api =
|
||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||
(Taro as any).requestMerchantTransfer
|
||
|
||
return typeof api === 'function'
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
||
if (!canRequestMerchantTransferConfirm()) {
|
||
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
||
}
|
||
|
||
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
||
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
||
|
||
const api =
|
||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||
(Taro as any).requestMerchantTransfer
|
||
|
||
if (typeof api !== 'function') {
|
||
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
api({
|
||
// WeChat API uses `package`, backend returns `package_info`.
|
||
package: cleanPackageInfo,
|
||
mchId: '1737910695',
|
||
appId: 'wxad831ba00ad6a026',
|
||
success: (res: any) => resolve(res),
|
||
fail: (err: any) => reject(err)
|
||
})
|
||
})
|
||
}
|
||
|
||
// Some backends may return money fields as number; keep internal usage always as string.
|
||
const normalizeMoneyString = (money: unknown) => {
|
||
if (money === null || money === undefined || money === '') return '0.00'
|
||
return typeof money === 'string' ? money : String(money)
|
||
}
|
||
|
||
const DealerWithdraw: React.FC = () => {
|
||
const [activeTab, setActiveTab] = useState<string | number>('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()
|
||
|
||
// Tab 切换处理函数
|
||
const handleTabChange = (value: string | number) => {
|
||
console.log('Tab切换到:', value)
|
||
setActiveTab(value)
|
||
|
||
// 如果切换到提现记录页面,刷新数据
|
||
if (String(value) === '1') {
|
||
fetchWithdrawRecords()
|
||
}
|
||
}
|
||
|
||
// 获取可提现余额
|
||
const fetchBalance = useCallback(async () => {
|
||
console.log(dealerUser, 'dealerUser...')
|
||
try {
|
||
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
||
} catch (error) {
|
||
console.error('获取余额失败:', error)
|
||
}
|
||
}, [dealerUser])
|
||
|
||
// 获取提现记录
|
||
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
|
||
}
|
||
|
||
if (!values.accountType) {
|
||
Taro.showToast({
|
||
title: '请选择提现方式',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证提现金额
|
||
const amount = parseFloat(String(values.amount))
|
||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||
|
||
if (isNaN(amount) || amount <= 0) {
|
||
Taro.showToast({
|
||
title: '请输入有效的提现金额',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (amount < 100) {
|
||
// Taro.showToast({
|
||
// title: '最低提现金额为100元',
|
||
// icon: 'error'
|
||
// })
|
||
// return
|
||
}
|
||
|
||
if (amount > available) {
|
||
Taro.showToast({
|
||
title: '提现金额超过可用余额',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证账户信息
|
||
if (values.accountType === 'alipay') {
|
||
if (!values.account || !values.accountName) {
|
||
Taro.showToast({
|
||
title: '请填写完整的支付宝信息',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
} else if (values.accountType === 'bank') {
|
||
if (!values.account || !values.accountName || !values.bankName) {
|
||
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 || '银行卡'
|
||
}
|
||
|
||
// WeChat wallet: backend should return `package_info`, frontend opens the "confirm receipt" page
|
||
// for user to click "确认收款".
|
||
if (values.accountType === 'wechat') {
|
||
if (!canRequestMerchantTransferConfirm()) {
|
||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
||
}
|
||
|
||
const createResult = await addShopDealerWithdraw(withdrawData)
|
||
const packageInfo = extractPackageInfo(createResult)
|
||
if (!packageInfo) {
|
||
throw new Error('后台未返回 package_info,无法调起微信收款确认页')
|
||
}
|
||
|
||
try {
|
||
await requestMerchantTransferConfirm(packageInfo)
|
||
Taro.showToast({
|
||
title: '已调起收款确认页',
|
||
icon: 'success'
|
||
})
|
||
} catch (e: any) {
|
||
const msg = String(e?.errMsg || e?.message || '')
|
||
if (/cancel/i.test(msg)) {
|
||
Taro.showToast({title: '已取消收款确认', icon: 'none'})
|
||
} else {
|
||
// Keep the original WeChat error for troubleshooting (e.g. "商户号错误").
|
||
throw new Error(msg || '调起收款确认页失败,请稍后重试')
|
||
}
|
||
}
|
||
} else {
|
||
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 = ['1','100', '300', '500', '1000']
|
||
|
||
const setQuickAmount = (amount: string) => {
|
||
formRef.current?.setFieldsValue({amount})
|
||
}
|
||
|
||
const setAllAmount = () => {
|
||
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
||
}
|
||
|
||
// 格式化金额
|
||
const formatMoney = (money?: unknown) => {
|
||
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||
}
|
||
|
||
const renderWithdrawForm = () => (
|
||
<View>
|
||
{/* 余额卡片 */}
|
||
<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 className={'flex flex-col'}>
|
||
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
|
||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</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"
|
||
onChange={(value) => {
|
||
// 实时验证提现金额
|
||
const amount = parseFloat(String(value))
|
||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||
if (!isNaN(amount) && amount > available) {
|
||
// 可以在这里添加实时提示,但不阻止输入
|
||
}
|
||
}}
|
||
/>
|
||
</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={(value) => {
|
||
const next = String(value)
|
||
setSelectedAccount(next)
|
||
// Ensure Form gets the field value even when Radio.Group is controlled.
|
||
formRef.current?.setFieldsValue({accountType: next})
|
||
}}
|
||
>
|
||
<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 = () => {
|
||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||
|
||
return (
|
||
<PullToRefresh
|
||
disabled={refreshing}
|
||
onRefresh={handleRefresh}
|
||
>
|
||
<View>
|
||
{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="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||
<View className="flex justify-between items-start mb-3">
|
||
<Space>
|
||
<Text className="font-semibold text-gray-800 mb-1">
|
||
提现金额:¥{record.money}
|
||
</Text>
|
||
<Text className="text-sm text-gray-500">
|
||
提现账户:{record.accountDisplay}
|
||
</Text>
|
||
</Space>
|
||
<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={handleTabChange}>
|
||
<Tabs.TabPane title="申请提现" value="0">
|
||
{renderWithdrawForm()}
|
||
</Tabs.TabPane>
|
||
|
||
<Tabs.TabPane title="提现记录" value="1">
|
||
{renderWithdrawRecords()}
|
||
</Tabs.TabPane>
|
||
</Tabs>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default DealerWithdraw
|