feat(app): 添加应用入口文件和配置文件
- 新增 index.tsx 作为应用的主入口文件 - 新增 index.config.ts 用于应用的配置管理
This commit is contained in:
5
src/passport/pay/index.config.ts
Normal file
5
src/passport/pay/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '确认支付',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
|
}
|
||||||
280
src/passport/pay/index.tsx
Normal file
280
src/passport/pay/index.tsx
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'
|
||||||
|
import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付页面
|
||||||
|
* 接收参数 subscriptionNo,完成 JSAPI 支付
|
||||||
|
*/
|
||||||
|
interface SubscriptionDetail {
|
||||||
|
id: number
|
||||||
|
subscriptionNo: string
|
||||||
|
productId: number
|
||||||
|
productName: string
|
||||||
|
productLogo?: string
|
||||||
|
productIcon?: string
|
||||||
|
status: string
|
||||||
|
priceType: string
|
||||||
|
payPrice: number
|
||||||
|
subscriptionPeriod?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsapiPayParams {
|
||||||
|
timeStamp: string
|
||||||
|
nonceStr: string
|
||||||
|
package: string
|
||||||
|
signType: string
|
||||||
|
paySign: string
|
||||||
|
outTradeNo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PayPage: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [paying, setPaying] = useState(false)
|
||||||
|
const [detail, setDetail] = useState<SubscriptionDetail | null>(null)
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
const [paid, setPaid] = useState(false)
|
||||||
|
|
||||||
|
// 从页面参数中获取 subscriptionNo
|
||||||
|
const getSubscriptionNo = useCallback(() => {
|
||||||
|
const instance = Taro.getCurrentInstance()
|
||||||
|
const params = instance?.router?.params || {}
|
||||||
|
return params.subscriptionNo || ''
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 获取订阅详情
|
||||||
|
const fetchDetail = useCallback(async () => {
|
||||||
|
const subscriptionNo = getSubscriptionNo()
|
||||||
|
if (!subscriptionNo) {
|
||||||
|
setErrorMsg('缺少订单参数')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await request({
|
||||||
|
url: `/app/subscription/detail-by-no/${subscriptionNo}`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res?.code === 200 || res?.code === 0) {
|
||||||
|
const data = res.data || res
|
||||||
|
|
||||||
|
if (data.status === 'active') {
|
||||||
|
setPaid(true)
|
||||||
|
setDetail(data)
|
||||||
|
} else {
|
||||||
|
setDetail(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrorMsg(res?.message || '获取订单信息失败')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMsg(err?.message || '网络异常,请重试')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [getSubscriptionNo])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDetail()
|
||||||
|
}, [fetchDetail])
|
||||||
|
|
||||||
|
// 执行支付
|
||||||
|
const handlePay = async () => {
|
||||||
|
if (!detail) return
|
||||||
|
|
||||||
|
setPaying(true)
|
||||||
|
try {
|
||||||
|
// 1. 获取登录凭证(code)并换取 openid
|
||||||
|
const loginRes = await Taro.login()
|
||||||
|
if (!loginRes.code) {
|
||||||
|
throw new Error('获取登录凭证失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 通过 code 获取 openid(使用现有的 loginByOpenId 接口)
|
||||||
|
const openid = Taro.getStorageSync('openid') || ''
|
||||||
|
|
||||||
|
// 3. 请求后端创建 JSAPI 预支付订单
|
||||||
|
const prepayRes: any = await request({
|
||||||
|
url: `/app/subscription/mp-prepay/${detail.id}`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { openid }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (prepayRes?.code !== 200 && prepayRes?.code !== 0) {
|
||||||
|
throw new Error(prepayRes?.message || '创建支付订单失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const payParams: JsapiPayParams = prepayRes.data || prepayRes
|
||||||
|
|
||||||
|
if (!payParams.timeStamp || !payParams.package || !payParams.paySign) {
|
||||||
|
throw new Error('支付参数不完整')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用微信支付
|
||||||
|
await Taro.requestPayment({
|
||||||
|
timeStamp: payParams.timeStamp,
|
||||||
|
nonceStr: payParams.nonceStr,
|
||||||
|
package: payParams.package,
|
||||||
|
signType: (payParams.signType || 'RSA') as any,
|
||||||
|
paySign: payParams.paySign
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. 支付成功后通知后端确认
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
} catch (confirmErr) {
|
||||||
|
console.warn('支付确认通知失败,等待轮询:', confirmErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 显示成功状态
|
||||||
|
setPaid(true)
|
||||||
|
Taro.showToast({ title: '支付成功', icon: 'success' })
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('支付失败:', err)
|
||||||
|
|
||||||
|
if (err?.errMsg?.includes('cancel')) {
|
||||||
|
Taro.showToast({ title: '支付已取消', icon: 'none' })
|
||||||
|
} else {
|
||||||
|
const msg = err?.message || err?.errMsg || '支付失败,请重试'
|
||||||
|
Taro.showToast({ title: msg, icon: 'error', duration: 2000 })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setPaying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const handleBack = () => {
|
||||||
|
Taro.navigateBack({ delta: 1 }).catch(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/user/user' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化价格描述
|
||||||
|
const getPriceDesc = () => {
|
||||||
|
if (!detail) return ''
|
||||||
|
if (detail.priceType === 'one_time') return '永久买断'
|
||||||
|
if (detail.priceType === 'subscription') {
|
||||||
|
return detail.subscriptionPeriod === 'year' ? '按年订阅' : '按月订阅'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 加载中 ---
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<View className="text-center">
|
||||||
|
<Clock className="text-blue-500 mb-4" size="48" />
|
||||||
|
<Text className="block text-gray-500">加载订单信息...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 错误 ---
|
||||||
|
if (errorMsg) {
|
||||||
|
return (
|
||||||
|
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<View className="text-center p-8">
|
||||||
|
<Close className="text-red-500 mb-4" size="48" />
|
||||||
|
<Text className="block text-gray-800 text-lg mb-2">获取订单失败</Text>
|
||||||
|
<Text className="block text-gray-500 mb-6">{errorMsg}</Text>
|
||||||
|
<Button type="primary" onClick={handleBack}>返回</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 已支付 ---
|
||||||
|
if (paid) {
|
||||||
|
return (
|
||||||
|
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<View className="text-center p-8">
|
||||||
|
<View className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Check className="text-green-500" size="40" />
|
||||||
|
</View>
|
||||||
|
<Text className="block text-green-600 text-xl font-bold mb-2">支付成功</Text>
|
||||||
|
<Text className="block text-gray-500 mb-2">{detail?.productName || '应用订阅'}</Text>
|
||||||
|
{detail?.payPrice ? (
|
||||||
|
<Text className="block text-gray-400 mb-6">
|
||||||
|
已支付 ¥{Number(detail.payPrice).toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Button type="primary" onClick={handleBack}>完成</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 支付确认 ---
|
||||||
|
return (
|
||||||
|
<View className="pay-page min-h-screen bg-gray-50">
|
||||||
|
<View className="p-4">
|
||||||
|
{/* 订单信息 */}
|
||||||
|
<View className="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||||
|
<Text className="block text-lg font-bold text-gray-800 mb-3">确认支付</Text>
|
||||||
|
|
||||||
|
<Cell title="商品名称" description={detail?.productName || '-'} />
|
||||||
|
<Cell title="价格类型" description={getPriceDesc()} />
|
||||||
|
|
||||||
|
<View className="flex items-center justify-between px-4 py-3">
|
||||||
|
<Text className="text-gray-600">支付金额</Text>
|
||||||
|
<Price
|
||||||
|
price={detail?.payPrice}
|
||||||
|
size="large"
|
||||||
|
thousands
|
||||||
|
color="#e53e3e"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<View className="flex items-start px-4 py-2">
|
||||||
|
<Tips className="text-orange-500 mr-2 mt-0.5" size="16" />
|
||||||
|
<Text className="text-sm text-gray-500">
|
||||||
|
支付成功后即可在「已购产品」中使用该应用
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 支付按钮 */}
|
||||||
|
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
|
||||||
|
<View className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
loading={paying}
|
||||||
|
onClick={handlePay}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{paying ? '支付中...' : '立即支付'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部安全距离占位 */}
|
||||||
|
<View style={{ height: '100px' }} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PayPage
|
||||||
Reference in New Issue
Block a user