add wxpay

This commit is contained in:
Felix
2025-12-08 10:40:01 +08:00
parent 9cc48e1972
commit 028c82f284
12 changed files with 296 additions and 17 deletions

View File

@@ -10,7 +10,8 @@
"pages/terms/terms",
"pages/privacy/privacy",
"pages/analyze/analyze",
"pages/coupon/coupon"
"pages/coupon/coupon",
"pages/order/order"
],
"window": {
"navigationBarTextStyle": "black",
@@ -25,4 +26,4 @@
"useExtendedLib" : {
"weui": true
}
}
}

View File

@@ -78,7 +78,7 @@ App<IAppOption>({
this.globalData.token = authInfo.token
this.globalData.userInfo = authInfo.userInfo
// 初始化词典等级
this.globalData.dictLevel = authInfo.dictLevel || 'PRIMARY'
this.globalData.dictLevel = authInfo.dictLevel || 'LEVEL1'
console.log('登录状态有效,自动登录')
} else {
console.log('Token 已过期,清理本地数据')
@@ -101,7 +101,7 @@ App<IAppOption>({
this.globalData.token = loginData.access_token
this.globalData.userInfo = loginData.userInfo
// 更新词典等级
this.globalData.dictLevel = loginData.dict_level || 'PRIMARY'
this.globalData.dictLevel = loginData.dict_level || 'LEVEL1'
// 存储到本地
wx.setStorageSync('token', loginData.access_token)

View File

@@ -1,6 +1,6 @@
<!--coupon.wxml-->
<view class="coupon-container">
<view class='coupon_box' wx:for="{{products}}" wx:key="id" wx:for-item="item">
<view class='coupon_box {{item.one_time ? "one_time" : ""}}' wx:for="{{products}}" wx:key="id" wx:for-item="item">
<view class='content' bindtap="handleCouponTap" data-id="{{item.id}}">
<view class='title'>{{item.title}}</view>
<view class='how_much'>{{item.points}}</view>

View File

@@ -11,7 +11,7 @@
}
.coupon_box{
background: linear-gradient(to right, #FF4B2B, #FF416C);
background: linear-gradient(to right, #3e9eff, #5cbffc);
width: 40%;
border-radius: 12rpx;
text-align: center;
@@ -21,6 +21,10 @@ background: linear-gradient(to right, #FF4B2B, #FF416C);
margin: 5% 5% 0 5%;
}
.coupon_box.one_time{
background: linear-gradient(to right, #FF4B2B, #FF416C);
}
.coupon_box::before{
content: '';
position: absolute;

View File

@@ -0,0 +1,16 @@
{
"usingComponents": {
"t-cell": "tdesign-miniprogram/cell/cell",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
"t-checkbox-group": "tdesign-miniprogram/checkbox-group/checkbox-group",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton"
},
"navigationBarTitleText": "订单",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#ffffff",
"backgroundTextStyle": "light",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}

View File

@@ -0,0 +1,148 @@
import apiManager from '../../utils/api'
Page({
data: {
orders: [] as Array<any>,
page: 1,
size: 20,
total: 0,
hasMore: true,
isLoading: false,
refundDialogVisible: false,
refundOrderId: '',
refundAmountCents: 0,
selectedReasons: [] as Array<string>,
reasonText: '',
reasonMax: 200
},
onLoad() {
this.loadOrders(1)
},
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadOrders(1).finally(() => wx.stopPullDownRefresh())
},
onReachBottom() {
if (this.data.hasMore && !this.data.isLoading) {
const next = this.data.page + 1
this.loadOrders(next)
}
},
async loadOrders(page: number) {
this.setData({ isLoading: page === 1 })
try {
const res = await apiManager.listOrders(page, this.data.size)
const items = (res.items || []).map((it: any) => ({
...it,
displayTime: this.formatTime(it.created_time),
displayAmount: this.formatAmount(it.amount_cents),
}))
console.log(items)
if (page === 1) {
this.setData({ orders: items, page, total: res.total || items.length, hasMore: page * this.data.size < (res.total || items.length), isLoading: false })
} else {
const merged = [...this.data.orders, ...items]
this.setData({ orders: merged, page, total: res.total || merged.length, hasMore: page * this.data.size < (res.total || merged.length), isLoading: false })
}
} catch (e) {
this.setData({ isLoading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
formatTime(t: string) {
try {
const d = new Date(t)
const y = d.getFullYear()
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const dd = `${d.getDate()}`.padStart(2, '0')
const hh = `${d.getHours()}`.padStart(2, '0')
const mm = `${d.getMinutes()}`.padStart(2, '0')
return `${y}-${m}-${dd} ${hh}:${mm}`
} catch (_) {
return t
}
},
formatAmount(cents: number) {
if (!cents && cents !== 0) return ''
return (cents / 100).toFixed(2)
},
async onRefundTap(e: any) {
const id = e.currentTarget.dataset.id
try {
const detail = await apiManager.getOrderDetail(id)
const updated = this.data.orders.map((it: any) => it.id === id ? {
...it,
refund_status: detail.refund_status || it.refund_status,
amount_cents: detail.amount_cents ?? it.amount_cents,
displayAmount: this.formatAmount(detail.amount_cents ?? it.amount_cents),
refundable_amount_cents: detail.refundable_amount_cents ?? it.refundable_amount_cents,
displayRefundable: this.formatAmount(detail.refundable_amount_cents ?? 0),
can_refund: detail.can_refund ?? it.can_refund
} : it)
this.setData({ orders: updated })
if (detail.refund_status) {
wx.showToast({ title: `退款状态:${detail.refund_status}`, icon: 'none' })
return
}
if (detail.can_refund) {
this.setData({ refundOrderId: id, refundAmountCents: Number(detail.refundable_amount_cents || 0), selectedReasons: [], reasonText: '', refundDialogVisible: true })
} else {
wx.showToast({ title: '当前订单不可退款', icon: 'none' })
}
} catch (_) {
wx.showToast({ title: '获取订单详情失败', icon: 'none' })
}
},
onReasonGroupChange(e: any) {
this.setData({ selectedReasons: e.detail.value || [] })
},
onReasonTextInput(e: any) {
const v = e.detail.value || ''
const max = this.data.reasonMax
this.setData({ reasonText: v.length > max ? v.slice(0, max) : v })
},
closeRefundDialog() {
this.setData({ refundDialogVisible: false })
},
async confirmRefundDialog() {
const sel = this.data.selectedReasons
const txt = (this.data.reasonText || '').trim()
if ((!sel || sel.length === 0) && !txt) {
wx.showToast({ title: '请至少选择或填写一项理由', icon: 'none' })
return
}
const reason = [...sel, txt].filter(Boolean).join('|')
try {
const r = await apiManager.refund(this.data.refundOrderId, reason, this.data.refundAmountCents)
wx.showToast({ title: '已提交退款申请', icon: 'none' })
this.setData({ refundDialogVisible: false })
const outRefundNo = r && (r.out_refund_no || (r.data && r.data.out_refund_no))
if (outRefundNo) {
setTimeout(async () => {
try {
const s = await apiManager.getRefundStatus(outRefundNo)
const status = s && (s.status || '')
const updated = this.data.orders.map((it: any) => it.id === this.data.refundOrderId ? { ...it, refund_status: status, can_refund: false } : it)
this.setData({ orders: updated })
wx.showToast({ title: status ? `退款状态:${status}` : '查询成功', icon: 'none' })
} catch (_) {
wx.showToast({ title: '查询失败', icon: 'none' })
}
}, 1000)
}
} catch (e) {
wx.showToast({ title: '提交失败', icon: 'none' })
}
}
})

View File

@@ -0,0 +1,39 @@
<view class="order-container">
<view wx:if="{{isLoading && page === 1}}" class="skeleton-wrap">
<t-skeleton theme="paragraph" animation="gradient" loading="{{true}}"></t-skeleton>
</view>
<view wx:elif="{{!isLoading && orders.length === 0}}" class="empty-wrap">没有记录</view>
<view wx:else class="order-list">
<block wx:for="{{orders}}" wx:key="id">
<t-cell title="{{item.displayTime}}" hover>
<view slot="description" class="order-desc">
¥{{item.displayAmount}} · 积分 {{item.points}}
<block wx:if="{{item.refundable_amount_cents > 0}}"> · 可退 ¥{{item.displayRefundable}}</block>
</view>
<view slot="right-icon">
<view wx:if="{{!item.refund_status}}" class="refund-btn" bindtap="onRefundTap" data-id="{{item.id}}">退款</view>
<view wx:else class="refund-status">{{item.refund_status}}</view>
</view>
</t-cell>
</block>
</view>
<t-dialog
visible="{{refundDialogVisible}}"
title="申请退款"
confirm-btn="提交"
cancel-btn="取消"
bind:confirm="confirmRefundDialog"
bind:cancel="closeRefundDialog"
>
<view slot="content" class="refund-content">
<t-checkbox-group value="{{selectedReasons}}" bind:change="onReasonGroupChange">
<t-checkbox value="误操作下单">误操作下单</t-checkbox>
<t-checkbox value="功能未达到预期">功能未达到预期</t-checkbox>
<t-checkbox value="重复支付或订单异常">重复支付或订单异常</t-checkbox>
</t-checkbox-group>
<textarea class="refund-textarea" value="{{reasonText}}" maxlength="{{reasonMax}}" placeholder="补充说明最多200字" bindinput="onReasonTextInput"></textarea>
<view class="reason-tip">至少选择一项或填写说明</view>
</view>
</t-dialog>
</view>

View File

@@ -0,0 +1,10 @@
.order-container { padding: 24rpx; }
.skeleton-wrap { padding: 24rpx; }
.empty-wrap { text-align: center; color: #999; padding: 80rpx 0; }
.order-list { display: flex; flex-direction: column; gap: 8rpx; }
.order-desc { color: #666; font-size: 26rpx; }
.refund-btn { color: #0052d9; font-size: 26rpx; padding: 8rpx 16rpx; border: 1rpx solid #0052d9; border-radius: 8rpx; }
.refund-status { color: #999; font-size: 26rpx; padding: 8rpx 0; }
.refund-content { padding: 12rpx 0; }
.refund-textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #ddd; border-radius: 8rpx; padding: 12rpx; box-sizing: border-box; margin-top: 12rpx; }
.reason-tip { color: #999; font-size: 24rpx; margin-top: 8rpx; }

View File

@@ -56,7 +56,7 @@ Page({
},
// 词典等级配置
dictLevel: 'PRIMARY',
dictLevel: 'LEVEL1',
dictLevelOptions: apiManager.getDictLevelOptions(),
// 应用信息
@@ -213,7 +213,7 @@ Page({
const userData = {
isLoggedIn: app.globalData.isLoggedIn || false,
userInfo: app.globalData.userInfo || null,
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
}
console.log('更新用户信息:', userData)

View File

@@ -74,12 +74,17 @@
<t-icon slot="left-icon" name="star" size="44rpx"></t-icon>
</t-cell>
</navigator>
<navigator url="/pages/order/order" class="cell-navigator">
<t-cell title="订单记录" hover arrow>
<t-icon slot="left-icon" name="shop" size="44rpx"></t-icon>
</t-cell>
</navigator>
<t-cell title="兑换码" hover arrow bindtap="showCouponDialog">
<t-icon slot="left-icon" name="coupon" size="44rpx"></t-icon>
</t-cell>
<t-cell title="消息" hover arrow>
<!-- <t-cell title="消息" hover arrow>
<t-icon slot="left-icon" name="notification" size="44rpx"></t-icon>
</t-cell>
</t-cell> -->
</view>
</view>

View File

@@ -827,10 +827,27 @@ Page({
console.error('跳转结果页面失败:', error)
this.setData({ isProcessing: false })
this.resetPageState()
wx.showToast({
title: '上传失败',
icon: 'none'
})
const msg = (error as any)?.message || ''
if (typeof msg === 'string' && msg.indexOf('积分') !== -1) {
wx.showModal({
title: '积分不足',
content: '您的积分不足,是否前往购买?',
confirmText: '购买',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/coupon/coupon' })
} else {
// wx.showToast({ title: '已取消', icon: 'none' })
}
}
})
} else {
wx.showToast({
title: msg || '上传失败',
icon: 'none'
})
}
}
},

View File

@@ -412,8 +412,25 @@ class ApiManager {
success: true
});
// 直接返回响应数据,由调用方处理业务逻辑
resolve(res.data);
if (res.statusCode === 200) {
// 直接返回响应数据,由调用方处理业务逻辑
resolve(res.data);
} else if (res.statusCode === 403) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('403 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else if (res.statusCode === 404 ) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('404 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('微信云托管请求错误:', errorMsg, response)
reject(new Error(errorMsg))
}
},
fail: (error: any) => {
if (showLoading) {
@@ -1785,7 +1802,7 @@ class ApiManager {
try {
console.log('开始获取图片文本和评分信息', { imageId });
const app = getApp<IAppOption>()
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
const response = await this.request<{
image_file_id: string,
assessments: Array<{
@@ -1925,6 +1942,28 @@ class ApiManager {
return response.data
}
async listOrders(page: number = 1, size: number = 20): Promise<{ items: Array<{ id: string; created_time: string; amount_cents: number; points?: number | null; product_id?: string | null; refund_status?: string | null }>; total: number }>{
const response = await this.request<{ items: Array<{ id: string; created_time: string; amount_cents: number; points?: number | null; product_id?: string | null; refund_status?: string | null }>; total: number }>(`/api/v1/wxpay/order/list`, 'POST', { page, size })
return response.data
}
async getOrderDetail(out_trade_no: string): Promise<{ out_trade_no: string; transaction_id?: string | null; trade_state: string; amount_cents: number; description: string; can_refund?: boolean | null; refund_status?: string | null; refundable_amount_cents?: number | null; refundable_amount_points?: number | null }>{
const response = await this.request<{ out_trade_no: string; transaction_id?: string | null; trade_state: string; amount_cents: number; description: string; can_refund?: boolean | null; refund_status?: string | null; refundable_amount_cents?: number | null; refundable_amount_points?: number | null }>(`/api/v1/wxpay/order/${out_trade_no}/details`, 'GET')
return response.data
}
async refund(out_trade_no: string, reason: string, amount_cents?: number): Promise<any> {
const payload: any = { out_trade_no, reason }
if (typeof amount_cents === 'number') payload.amount_cents = amount_cents
const response = await this.request<any>(`/api/v1/wxpay/refund`, 'POST', payload)
return response.data
}
async getRefundStatus(out_refund_no: string): Promise<{ out_refund_no: string; status: string }>{
const response = await this.request<{ out_refund_no: string; status: string }>(`/api/v1/wxpay/refund/${out_refund_no}`, 'GET')
return response.data
}
}
// 导出单例