- 新增 getGltTicketTemplateByGoodsId 函数用于查询水票模板 - 移除最低提现金额显示,保留手续费信息 - 隐藏订单确认页面中的配送范围设置区域 - 添加订单注意事项说明文本
558 lines
16 KiB
TypeScript
558 lines
16 KiB
TypeScript
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||
import {View, Text} from '@tarojs/components'
|
||
import {
|
||
Space,
|
||
Button,
|
||
Form,
|
||
Input,
|
||
CellGroup,
|
||
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,
|
||
receiveShopDealerWithdraw,
|
||
receiveSuccessShopDealerWithdraw
|
||
} from '@/api/shop/shopDealerWithdraw'
|
||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||
|
||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||
accountDisplay?: string
|
||
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
||
package_info?: string
|
||
packageInfo?: string
|
||
package?: string
|
||
}
|
||
|
||
const extractPackageInfo = (result: unknown): string | null => {
|
||
if (typeof result === 'string') return result
|
||
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 [loading, setLoading] = useState<boolean>(false)
|
||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||
const [claimingId, setClaimingId] = useState<number | null>(null)
|
||
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 'info'
|
||
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(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
|
||
}
|
||
|
||
try {
|
||
setSubmitting(true)
|
||
|
||
const withdrawData: ShopDealerWithdraw = {
|
||
userId: dealerUser.userId,
|
||
money: values.amount,
|
||
// Only support WeChat wallet withdrawals.
|
||
payType: 10,
|
||
applyStatus: 10, // 待审核
|
||
platform: 'MiniProgram'
|
||
}
|
||
|
||
// Security flow:
|
||
// 1) user submits => applyStatus=10 (待审核)
|
||
// 2) backend审核通过 => applyStatus=20 (待领取)
|
||
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
||
await addShopDealerWithdraw(withdrawData)
|
||
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
||
|
||
// 重置表单
|
||
formRef.current?.resetFields()
|
||
|
||
// 刷新数据
|
||
await handleRefresh()
|
||
|
||
// 切换到提现记录页面
|
||
setActiveTab('1')
|
||
|
||
} catch (error: any) {
|
||
console.error('提现申请失败:', error)
|
||
Taro.showToast({
|
||
title: error.message || '提现申请失败',
|
||
icon: 'error'
|
||
})
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
const handleClaim = async (record: WithdrawRecordWithDetails) => {
|
||
if (!record?.id) {
|
||
Taro.showToast({title: '记录不存在', icon: 'error'})
|
||
return
|
||
}
|
||
|
||
if (record.applyStatus !== 20) {
|
||
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
|
||
return
|
||
}
|
||
|
||
if (record.payType !== 10) {
|
||
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
|
||
return
|
||
}
|
||
|
||
if (claimingId !== null) return
|
||
|
||
try {
|
||
setClaimingId(record.id)
|
||
|
||
if (!canRequestMerchantTransferConfirm()) {
|
||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
||
}
|
||
|
||
const receiveResult = await receiveShopDealerWithdraw(record.id)
|
||
const packageInfo = extractPackageInfo(receiveResult)
|
||
if (!packageInfo) {
|
||
throw new Error('后台未返回 package_info,无法领取,请联系管理员')
|
||
}
|
||
|
||
try {
|
||
await requestMerchantTransferConfirm(packageInfo)
|
||
} catch (e: any) {
|
||
const msg = String(e?.errMsg || e?.message || '')
|
||
if (/cancel/i.test(msg)) {
|
||
Taro.showToast({title: '已取消领取', icon: 'none'})
|
||
return
|
||
}
|
||
throw new Error(msg || '领取失败,请稍后重试')
|
||
}
|
||
|
||
try {
|
||
await receiveSuccessShopDealerWithdraw(record.id)
|
||
Taro.showToast({title: '领取成功', icon: 'success'})
|
||
} catch (e: any) {
|
||
console.warn('领取成功,但状态同步失败:', e)
|
||
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
|
||
} finally {
|
||
await handleRefresh()
|
||
}
|
||
} catch (e: any) {
|
||
console.error('领取失败:', e)
|
||
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
|
||
} finally {
|
||
setClaimingId(null)
|
||
}
|
||
}
|
||
|
||
const quickAmounts = ['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">
|
||
手续费:免费
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<Form
|
||
ref={formRef}
|
||
onFinish={handleSubmit}
|
||
labelPosition="top"
|
||
>
|
||
<CellGroup>
|
||
<Form.Item name="amount" label="提现金额" required>
|
||
<Input
|
||
placeholder="请输入提现金额"
|
||
type="number"
|
||
/>
|
||
</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>
|
||
|
||
<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}
|
||
>
|
||
{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 background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
||
{getStatusText(record.applyStatus)}
|
||
</Tag>
|
||
</View>
|
||
|
||
|
||
{record.applyStatus === 20 && record.payType === 10 && (
|
||
<View className="flex mb-5 justify-center">
|
||
<Button
|
||
size="small"
|
||
type="primary"
|
||
loading={claimingId === record.id}
|
||
disabled={claimingId !== null}
|
||
onClick={() => handleClaim(record)}
|
||
>
|
||
立即领取
|
||
</Button>
|
||
</View>
|
||
)}
|
||
|
||
<View className="flex justify-between items-center">
|
||
<View className="text-xs text-gray-400">
|
||
<Text>创建时间:{record.createTime}</Text>
|
||
{record.auditTime && (
|
||
<Text className="block mt-1">
|
||
审核时间:{record.auditTime}
|
||
</Text>
|
||
)}
|
||
{record.rejectReason && (
|
||
<Text className="block mt-1 text-red-500">
|
||
驳回原因:{record.rejectReason}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
</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
|