feat(app): 添加应用入口文件和配置文件

- 新增 index.tsx 作为应用的主入口文件
- 新增 index.config.ts 用于应用的配置管理
This commit is contained in:
2026-06-30 15:27:41 +08:00
parent ec084e39ba
commit c39ee72c73
2 changed files with 285 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '确认支付',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
}

280
src/passport/pay/index.tsx Normal file
View 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