邹方成 be57eda392 fix(orders): 修复订单列表显示问题并优化详情页展示
修复订单列表不显示 source_type=3 订单的问题,支持对对碰等玩法订单
优化订单标题显示逻辑,移除内部标识并添加保底显示
优化订单详情页,当没有实物商品时显示活动信息
重构订单类型判断逻辑,支持更多玩法类型
2025-12-22 14:40:53 +08:00

797 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container">
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="error" class="error-state">
<view class="error-icon"></view>
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @tap="loadOrder">重试</button>
</view>
<!-- 订单内容 -->
<view v-else-if="order" class="content">
<!-- 状态头部背景 -->
<view class="status-header-bg" :class="getStatusClass(order)">
<view class="bg-circle c1"></view>
<view class="bg-circle c2"></view>
</view>
<!-- 状态卡片 -->
<view class="status-card">
<view class="status-content">
<view class="status-icon-wrap">
<text class="status-icon">{{ getStatusIcon(order) }}</text>
</view>
<view class="status-info">
<text class="status-title">{{ statusText(order) }}</text>
<text class="status-desc" v-if="order.status === 1">请在 15 分钟内完成支付</text>
<text class="status-desc" v-else-if="order.status === 3">订单已取消</text>
<text class="status-desc" v-else>感谢您的购买期待再次光临</text>
</view>
</view>
</view>
<!-- 奖品/商品列表 -->
<view class="section-card product-section">
<view class="section-header">
<text class="section-title">商品清单</text>
<text class="item-count"> {{ order.items ? order.items.length : (order.activity_name ? 1 : 0) }} </text>
</view>
<view class="order-items">
<!-- 常规商品列表 -->
<view v-for="(item, index) in order.items" :key="index" class="item-card">
<view class="item-image-wrap">
<image class="item-image" :src="getProductImage(item)" mode="aspectFill" />
<!-- 购买标识 -->
<view class="winner-tag" v-if="order.is_winner && (order.source_type === 2 || order.source_type === 3)">
<text class="tag-text">已开启</text>
</view>
<view class="level-tag" v-if="order.reward_level">
<text class="tag-text">{{ order.reward_level }}</text>
</view>
</view>
<view class="item-info">
<text class="item-title">{{ item.title || '商品' }}</text>
<view class="item-tags" v-if="order.activity_name">
<text class="tag">{{ order.activity_name }}</text>
</view>
<view class="item-meta">
<view class="price-wrap">
<text class="currency">¥</text>
<text class="price">{{ formatPrice(item.price) }}</text>
</view>
<text class="item-quantity">x{{ item.quantity }}</text>
</view>
</view>
</view>
<!-- 活动信息当没有实物商品时显示 -->
<view v-if="(!order.items || order.items.length === 0) && order.activity_name" class="item-card">
<view class="item-image-wrap">
<image class="item-image" :src="defaultImage" mode="aspectFill" />
<view class="winner-tag" v-if="order.is_winner">
<text class="tag-text">已开启</text>
</view>
</view>
<view class="item-info">
<text class="item-title">{{ order.activity_name }}</text>
<view class="item-tags">
<text class="tag">参与记录</text>
<text class="tag" v-if="order.issue_number">{{ order.issue_number }}</text>
</view>
<view class="item-meta">
<view class="price-wrap">
<text class="currency">¥</text>
<text class="price">{{ formatPrice(order.actual_amount) }}</text>
</view>
<text class="item-quantity">x1</text>
</view>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="section-card info-section">
<view class="info-row">
<text class="label">订单编号</text>
<view class="value-wrap">
<text class="value mono">{{ order.order_no }}</text>
<view class="copy-btn" @tap="copyText(order.order_no)">复制</view>
</view>
</view>
<view class="info-row">
<text class="label">下单时间</text>
<text class="value">{{ formatTime(order.created_at) }}</text>
</view>
<view class="info-row" v-if="order.paid_at">
<text class="label">支付时间</text>
<text class="value">{{ formatTime(order.paid_at) }}</text>
</view>
<view class="info-row" v-if="order.cancelled_at">
<text class="label">取消时间</text>
<text class="value">{{ formatTime(order.cancelled_at) }}</text>
</view>
<view class="info-row">
<text class="label">订单来源</text>
<text class="value">{{ getSourceTypeText(order.source_type) }}</text>
</view>
</view>
<!-- 金额明细 -->
<view class="section-card amount-section">
<view class="info-row">
<text class="label">商品总额</text>
<text class="value">¥{{ formatPrice(order.total_amount) }}</text>
</view>
<view class="info-row" v-if="order.discount_amount">
<text class="label">优惠金额</text>
<text class="value discount">-¥{{ formatPrice(order.discount_amount) }}</text>
</view>
<view class="divider"></view>
<view class="total-row">
<text class="total-label">实付款</text>
<view class="total-price-wrap">
<text class="currency">¥</text>
<text class="total-price">{{ formatPrice(order.actual_amount) }}</text>
</view>
</view>
</view>
<!-- 抽奖凭证有凭证数据时显示 -->
<view class="section-card proof-section" v-if="order.draw_receipts && order.draw_receipts.length > 0">
<view class="section-header">
<text class="section-title">抽奖凭证</text>
<text class="item-count help-btn" @tap="showProofHelp">?</text>
</view>
<view v-for="(receipt, idx) in order.draw_receipts" :key="idx" class="receipt-block">
<view class="info-row" v-if="receipt.algo_version">
<text class="label">算法版本</text>
<text class="value mono">{{ receipt.algo_version }}</text>
</view>
<view class="info-row" v-if="receipt.server_seed_hash">
<text class="label">服务端种子哈希</text>
<view class="value-wrap">
<text class="value mono seed-text">{{ receipt.server_seed_hash }}</text>
<view class="copy-btn" @tap="copyText(receipt.server_seed_hash)">复制</view>
</view>
</view>
<view class="info-row" v-if="receipt.server_sub_seed">
<text class="label">子种子</text>
<view class="value-wrap">
<text class="value mono seed-text">{{ receipt.server_sub_seed }}</text>
<view class="copy-btn" @tap="copyText(receipt.server_sub_seed)">复制</view>
</view>
</view>
<view class="info-row" v-if="receipt.client_seed">
<text class="label">客户端种子</text>
<text class="value mono">{{ receipt.client_seed }}</text>
</view>
<view class="info-row" v-if="receipt.draw_id">
<text class="label">抽奖ID</text>
<text class="value mono">{{ receipt.draw_id }}</text>
</view>
<view class="info-row" v-if="receipt.timestamp">
<text class="label">时间戳</text>
<text class="value mono">{{ receipt.timestamp }}</text>
</view>
<view class="info-row" v-if="receipt.round_id">
<text class="label">期次ID</text>
<text class="value">{{ receipt.round_id }}</text>
</view>
</view>
<view class="proof-notice">
<text class="notice-icon">🔒</text>
<text class="notice-text">以上数据可用于验证抽奖结果的公正性</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="footer-actions safe-area-bottom" v-if="order && order.status === 1">
<view class="action-btn secondary" @tap="handleCancel">取消订单</view>
<view class="action-btn primary" @tap="handlePay">立即支付</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getOrderDetail, cancelOrder, createWechatOrder } from '../../api/appUser'
const orderId = ref('')
const order = ref(null)
const loading = ref(true)
const error = ref('')
const defaultImage = 'https://keaiya-1259195914.cos.ap-shanghai.myqcloud.com/images/default-product.png'
onLoad((options) => {
if (options.id) {
orderId.value = options.id
loadOrder()
} else {
error.value = '参数错误'
loading.value = false
}
})
async function loadOrder() {
loading.value = true
error.value = ''
try {
const res = await getOrderDetail(orderId.value)
order.value = res
} catch (e) {
error.value = e.message || '获取订单详情失败'
} finally {
loading.value = false
}
}
function handleCancel() {
uni.showModal({
title: '确认取消',
content: '确定要取消这个订单吗?',
confirmColor: '#FF6B00',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '取消中...' })
try {
await cancelOrder(orderId.value, '用户主动取消')
uni.hideLoading()
uni.showToast({ title: '订单已取消', icon: 'success' })
loadOrder()
} catch (e) {
uni.hideLoading()
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
}
}
}
})
}
function handlePay() {
const openid = uni.getStorageSync('openid')
if (!openid) {
uni.showToast({ title: '缺少OpenID请重新登录', icon: 'none' })
return
}
const ord = order.value
if (!ord || !ord.order_no) return
uni.showLoading({ title: '拉起支付...' })
createWechatOrder({ openid, order_no: ord.order_no })
.then((payRes) => new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payRes.timeStamp || payRes.timestamp,
nonceStr: payRes.nonceStr || payRes.noncestr,
package: payRes.package,
signType: payRes.signType || 'MD5',
paySign: payRes.paySign,
success: resolve,
fail: reject
})
}))
.then(async () => {
uni.hideLoading()
uni.showToast({ title: '支付成功', icon: 'success' })
await loadOrder()
})
.catch((e) => {
uni.hideLoading()
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
uni.showToast({ title: '支付已取消', icon: 'none' })
return
}
uni.showToast({ title: e?.message || '支付失败', icon: 'none' })
})
}
function copyText(text) {
if (!text) return
uni.setClipboardData({
data: String(text),
success: () => {
uni.showToast({ title: '已复制', icon: 'none' })
}
})
}
function formatPrice(price) {
if (price === undefined || price === null) return '0.00'
return (Number(price) / 100).toFixed(2)
}
function formatTime(t) {
if (!t) return ''
const date = new Date(t)
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const hh = String(date.getHours()).padStart(2, '0')
const mm = String(date.getMinutes()).padStart(2, '0')
const ss = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}
function getProductImage(item) {
if (item.product_images) {
try {
const parsed = JSON.parse(item.product_images)
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed[0]
}
} catch (e) {
if (typeof item.product_images === 'string' && item.product_images.startsWith('http')) {
return item.product_images
}
}
}
return defaultImage
}
function statusText(item) {
const status = item.status
if (status === 1) return '待付款'
if (status === 2) return '已完成'
if (status === 3) return '已取消'
return '进行中'
}
function getStatusClass(item) {
const status = item.status
if (status === 1) return 'status-pending'
if (status === 2) return 'status-completed'
if (status === 3) return 'status-cancelled'
return ''
}
function getStatusIcon(item) {
const status = item.status
if (status === 1) return '🕒'
if (status === 2) return '🎉'
if (status === 3) return '🚫'
return '📦'
}
function getSourceTypeText(type) {
if (type === 1) return '商城订单'
if (type === 2 || type === 3) {
// 优先使用分类名称,其次活动名称,最后根据玩法类型显示
if (order.value && order.value.category_name) return order.value.category_name
if (order.value && order.value.activity_name) return order.value.activity_name
const playType = order.value && order.value.play_type
if (playType === 'match') return '对对碰'
if (playType === 'ichiban') return '一番赏'
if (type === 2) return '抽奖订单'
return '发奖记录'
}
return '其他'
}
function showProofHelp() {
uni.showModal({
title: '抽奖凭证说明',
content: '该凭证包含本次抽奖的随机种子和参数,可用于验证抽奖的公平性。您可以复制相关数据,自行进行核验。',
showCancel: false
})
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: $bg-page;
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
position: relative;
}
/* 状态头部背景 */
.status-header-bg {
height: 150rpx;
position: relative;
overflow: hidden;
border-radius: 0 0 40rpx 40rpx;
&.status-pending { background: linear-gradient(135deg, #FF9F43 0%, #FF6B6B 100%); }
&.status-completed { background: linear-gradient(135deg, #2ECC71 0%, #27AE60 100%); }
&.status-cancelled { background: linear-gradient(135deg, #95A5A6 0%, #7F8C8D 100%); }
.bg-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.c1 { width: 300rpx; height: 300rpx; top: -100rpx; right: -50rpx; }
&.c2 { width: 200rpx; height: 200rpx; bottom: 50rpx; left: -50rpx; }
}
}
/* 状态卡片 */
.status-card {
margin: -60rpx $spacing-lg 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: $radius-xl;
padding: $spacing-xl;
box-shadow: $shadow-card;
position: relative;
z-index: 10;
.status-content {
display: flex;
align-items: center;
gap: $spacing-lg;
}
.status-icon-wrap {
width: 88rpx;
height: 88rpx;
background: $bg-secondary;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.status-icon {
font-size: 44rpx;
}
.status-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.status-title {
font-size: 36rpx;
font-weight: 800;
color: $text-main;
}
.status-desc {
font-size: $font-sm;
color: $text-sub;
}
}
/* 通用卡片样式 */
.section-card {
margin: $spacing-lg;
background: $bg-card;
border-radius: $radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
animation: slideUp 0.4s ease-out;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-lg;
padding-bottom: $spacing-sm;
border-bottom: 2rpx dashed $border-color-light;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: $text-main;
position: relative;
padding-left: 20rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 24rpx;
background: $brand-primary;
border-radius: 4rpx;
}
}
.item-count {
font-size: $font-sm;
color: $text-sub;
}
/* 商品列表 */
.item-card {
display: flex;
gap: $spacing-md;
margin-bottom: $spacing-lg;
&:last-child { margin-bottom: 0; }
}
.item-image-wrap {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: $radius-md;
overflow: hidden;
background: $bg-secondary;
}
.item-image {
width: 100%;
height: 100%;
}
.winner-tag {
position: absolute;
top: 0;
left: 0;
background: $gradient-gold;
padding: 4rpx 12rpx;
border-radius: 0 0 $radius-md 0;
z-index: 1;
.tag-text {
color: #fff;
font-size: 18rpx;
font-weight: 700;
}
}
.level-tag {
position: absolute;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
padding: 2rpx 10rpx;
border-radius: $radius-sm 0 0 0;
.tag-text {
color: #fff;
font-size: 18rpx;
font-weight: 600;
}
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4rpx 0;
}
.item-title {
font-size: 28rpx;
color: $text-main;
font-weight: 600;
line-height: 1.4;
@include text-ellipsis(2);
}
.item-tags {
margin-top: 8rpx;
display: flex;
.tag {
font-size: 20rpx;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
padding: 2rpx 10rpx;
border-radius: 6rpx;
}
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: auto;
}
.price-wrap {
display: flex;
align-items: baseline;
color: $text-main;
.currency { font-size: 24rpx; font-weight: 600; }
.price { font-size: 32rpx; font-weight: 700; font-family: 'DIN Alternate', sans-serif; }
}
.item-quantity {
font-size: 24rpx;
color: $text-sub;
}
/* 信息行 */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
.label {
font-size: 26rpx;
color: $text-sub;
}
.value {
font-size: 26rpx;
color: $text-main;
&.mono { font-family: monospace; }
&.discount { color: $uni-color-error; font-weight: 600; }
}
.value-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.copy-btn {
font-size: 20rpx;
color: $text-sub;
border: 1rpx solid $border-color;
padding: 2rpx 12rpx;
border-radius: 20rpx;
&:active {
opacity: 0.6;
background: $bg-secondary;
}
}
}
.divider {
height: 1rpx;
background: $border-color-light;
margin: 20rpx 0;
}
.total-row {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16rpx;
padding-top: 10rpx;
.total-label {
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
.total-price-wrap {
color: $brand-primary;
display: flex;
align-items: baseline;
.currency { font-size: 28rpx; font-weight: 600; }
.total-price { font-size: 40rpx; font-weight: 800; font-family: 'DIN Alternate', sans-serif; }
}
}
/* 底部操作栏 */
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
display: flex;
justify-content: flex-end;
gap: 24rpx;
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
z-index: 100;
}
.action-btn {
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 48rpx;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 600;
transition: all 0.2s;
&:active { transform: scale(0.96); }
&.secondary {
background: #fff;
color: $text-main;
border: 2rpx solid $border-color;
}
&.primary {
background: $gradient-brand;
color: #fff;
box-shadow: $shadow-warm;
}
}
/* Loading & Error */
.loading-state, .error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80vh;
.loading-text, .error-text {
margin-top: 24rpx;
color: $text-sub;
font-size: 28rpx;
}
}
.loading-spinner {
width: 64rpx;
height: 64rpx;
border: 6rpx solid rgba($brand-primary, 0.2);
border-top-color: $brand-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.retry-btn {
margin-top: 32rpx;
background: $brand-primary;
color: #fff;
font-size: 28rpx;
padding: 12rpx 48rpx;
border-radius: 32rpx;
}
/* 抽奖凭证区 */
.proof-section {
.seed-text {
font-size: 22rpx;
word-break: break-all;
max-width: 360rpx;
@include text-ellipsis(1);
}
.proof-notice {
display: flex;
align-items: center;
gap: $spacing-sm;
margin-top: $spacing-md;
padding: $spacing-sm $spacing-md;
background: rgba($brand-primary, 0.06);
border-radius: $radius-md;
.notice-icon {
font-size: 24rpx;
}
.notice-text {
font-size: 22rpx;
color: $text-sub;
}
}
}
</style>