932 lines
23 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 class="bg-decoration"></view>
<!-- 顶部 Tab -->
<view class="tabs glass-card">
<view
class="tab-item"
:class="{ active: currentTab === 'pending' }"
@click="switchTab('pending')"
>
<text class="tab-text">待付款</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'completed' }"
@click="switchTab('completed')"
>
<text class="tab-text">已完成</text>
</view>
</view>
<!-- 页面内容 -->
<view class="page-content">
<!-- 错误提示 -->
<view v-if="error" class="error-toast">
<text class="error-icon"></text>
<text class="error-text">{{ error }}</text>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view v-else-if="orders.length === 0" class="empty-state">
<view class="empty-illustration">
<text class="empty-icon">📦</text>
</view>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">{{ currentTab === 'pending' ? '没有待付款的订单' : '完成的订单将显示在这里' }}</text>
<button class="empty-btn" @tap="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="orders-list">
<view
v-for="(item, index) in orders"
:key="item.id || item.order_no"
class="order-card"
:style="{ '--delay': index * 0.05 + 's' }"
@tap="goOrderDetail(item)"
>
<!-- 订单头部 -->
<view class="order-header">
<view class="order-type">
<text class="type-icon">{{ getTypeIcon(item) }}</text>
<text class="type-name">{{ getTypeName(item) }}</text>
</view>
<view class="order-status" :class="getStatusClass(item)">
{{ statusText(item) }}
</view>
</view>
<!-- 订单内容 -->
<view class="order-body">
<!-- 商品图片 -->
<view class="product-image-wrap">
<image
class="product-image"
:src="getProductImage(item)"
mode="aspectFill"
/>
<view class="image-overlay" v-if="item.is_winner">
<text class="winner-badge">🎉 已开启</text>
</view>
</view>
<!-- 商品信息 -->
<view class="product-info">
<text class="product-title">{{ getOrderTitle(item) }}</text>
<view class="product-meta">
<text class="meta-item" v-if="item.activity_name">{{ item.activity_name }}</text>
<text class="meta-item" v-if="item.issue_number">{{ item.issue_number }}</text>
<text class="meta-item coupon-tag" v-if="item.coupon_info">: {{ item.coupon_info.name }}</text>
<text class="meta-item card-tag" v-if="item.item_card_info">: {{ item.item_card_info.name }}</text>
</view>
<text class="order-time">{{ formatTime(item.created_at) }}</text>
</view>
</view>
<!-- 订单底部 -->
<view class="order-footer">
<view class="order-no">
<text class="no-label">订单号:</text>
<text class="no-value">{{ item.order_no }}</text>
</view>
<view class="order-amount">
<text class="amount-label" v-if="shouldShowAmountLabel(item)">实付</text>
<text class="amount-value">{{ getAmountText(item) }}</text>
</view>
</view>
<!-- 快捷操作 -->
<view class="order-actions" v-if="currentTab === 'pending'">
<button class="action-btn secondary" @tap.stop="doCancelOrder(item)">取消订单</button>
<button class="action-btn primary" @tap.stop="payOrder(item)">立即支付</button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner small"></view>
<text>加载更多...</text>
</view>
<view v-else-if="!hasMore" class="no-more">
<view class="divider"></view>
<text>没有更多了</text>
<view class="divider"></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getOrders, cancelOrder as cancelOrderApi, createWechatOrder } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const currentTab = ref('pending')
const orders = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const error = ref('')
const page = ref(1)
const pageSize = ref(20)
const hasMore = ref(true)
// 默认商品图片
const defaultImage = 'https://keaiya-1259195914.cos.ap-shanghai.myqcloud.com/images/default-product.png'
function formatTime(t) {
if (!t) return ''
const d = typeof t === 'string' ? new Date(t) : new Date(t)
const now = new Date()
const diffMs = now - d
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
if (diffDays < 7) return `${diffDays}天前`
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${m}-${day} ${hh}:${mm}`
}
function formatAmount(a, item) {
if (item && item.points_amount > 0) {
return `${item.points_amount}积分`
}
if (a === undefined || a === null) return '¥0.00'
const n = Number(a)
if (Number.isNaN(n)) return '¥0.00'
const yuan = n / 100
return `¥${yuan.toFixed(2)}`
}
function shouldShowAmountLabel(item) {
const amount = item.actual_amount || item.total_amount
return amount > 0
}
function getAmountText(item) {
if (item.points_amount > 0) return formatAmount(0, item)
const amount = item.actual_amount || item.total_amount
if (amount > 0) return formatAmount(amount)
// 金额为0的情况
if (item.source_type === 3 || item.source_type === 2) {
return '奖品'
}
return '免费'
}
function getOrderTitle(item) {
// 1. 优先使用 items 中的商品名称(通常是实物购买或中奖)
if (item.items && item.items.length > 0 && item.items[0].title) {
return item.items[0].title
}
// 2. 其次使用活动名称(玩法类订单)
if (item.activity_name) {
return item.activity_name
}
// 3. 处理 remark过滤掉内部标识
if (item.remark) {
// 过滤掉内部标识(如 lottery:xxx, matching_game:xxx 等)
if (!item.remark.startsWith('lottery:') &&
!item.remark.startsWith('matching_game:') &&
!item.remark.includes(':issue:')) {
return item.remark
}
}
// 4. 保底显示
return item.title || item.subject || '盲盒订单'
}
function getProductImage(item) {
// 从 items 中获取图片
if (item.items && item.items.length > 0) {
const images = item.items[0].product_images
if (images) {
try {
const parsed = JSON.parse(images)
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed[0]
}
} catch (e) {
if (typeof images === 'string' && images.startsWith('http')) {
return images
}
}
}
}
return defaultImage
}
function getTypeIcon(item) {
const sourceType = item.source_type
if (sourceType === 2 || sourceType === 3) {
// 根据玩法类型显示不同图标
const playType = item.play_type
if (playType === 'match') return '🎮' // 对对碰
if (playType === 'ichiban') return '🎰' // 一番赏
if (sourceType === 2) return '🎲' // 默认抽奖
}
if (sourceType === 1) return '🛒' // 商城订单
return '📦'
}
function getTypeName(item) {
const sourceType = item.source_type
if (sourceType === 2 || sourceType === 3) {
// 优先使用分类名称,其次活动名称,最后根据玩法类型显示
if (item.category_name) return item.category_name
if (item.activity_name) return item.activity_name
const playType = item.play_type
if (playType === 'match') return '对对碰'
if (playType === 'ichiban') return '一番赏'
if (sourceType === 2) return '抽奖'
}
if (sourceType === 1) return '商城'
return '订单'
}
function statusText(item) {
// 检查是否已开奖
if (item.is_draw === true || item.is_draw === 1) {
return item.is_winner ? '已中奖' : '未中奖'
}
const status = item.status
if (status === 1) return '待付款'
if (status === 2) return '已完成'
if (status === 3) return '已取消'
return '进行中'
}
function getStatusClass(item) {
const text = statusText(item)
if (text === '待付款') return 'status-pending'
if (text === '已完成' || text === '已中奖') return 'status-success'
if (text === '未中奖') return 'status-normal'
if (text === '已取消') return 'status-cancelled'
return 'status-processing'
}
function switchTab(tab) {
if (currentTab.value === tab) return
vibrateShort()
currentTab.value = tab
fetchOrders(false)
}
function apiStatus() {
// 1: 待付款, 2: 已完成
return currentTab.value === 'pending' ? 1 : 2
}
// 过滤掉 source_type=3 的发奖订单
function filterOrders(items) {
if (!Array.isArray(items)) return []
// 不再过滤 source_type=3因为对对碰等玩法的订单也是 source_type=3
return items
}
async function fetchOrders(append) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
// 使用统一的手机号绑定检查
const hasPhoneBound = uni.getStorageSync('login_method') === 'wechat_phone' || uni.getStorageSync('login_method') === 'sms' || uni.getStorageSync('phone_number')
if (!user_id || !token || !hasPhoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
if (!append) {
if (currentTab.value === 'completed') {
await fetchAllOrders()
return
} else {
loading.value = true
page.value = 1
hasMore.value = true
orders.value = []
}
} else {
if (!hasMore.value || loadingMore.value) return
loadingMore.value = true
page.value = page.value + 1
}
error.value = ''
try {
const list = await getOrders(user_id, apiStatus(), page.value, pageSize.value)
const rawItems = Array.isArray(list) ? list : (list && list.items) || (list && list.list) || []
const items = filterOrders(rawItems)
const total = (list && list.total) || 0
orders.value = append ? orders.value.concat(items) : items
if (total) {
hasMore.value = orders.value.length < total
} else {
hasMore.value = items.length === pageSize.value
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取订单失败'
} finally {
if (append) {
loadingMore.value = false
} else {
loading.value = false
}
}
}
async function fetchAllOrders() {
const user_id = uni.getStorageSync('user_id')
loading.value = true
page.value = 1
hasMore.value = false
orders.value = []
try {
const first = await getOrders(user_id, apiStatus(), 1, pageSize.value)
const rawItemsFirst = Array.isArray(first) ? first : (first && first.items) || (first && first.list) || []
const itemsFirst = filterOrders(rawItemsFirst)
const total = (first && first.total) || 0
orders.value = itemsFirst
const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize.value))
for (let p = 2; p <= totalPages; p++) {
const res = await getOrders(user_id, apiStatus(), p, pageSize.value)
const rawItems = Array.isArray(res) ? res : (res && res.items) || (res && res.list) || []
const items = filterOrders(rawItems)
orders.value = orders.value.concat(items)
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取订单失败'
} finally {
loading.value = false
}
}
function goOrderDetail(item) {
// 跳转订单详情页
uni.navigateTo({
url: `/pages-user/orders/detail?id=${item.id}&order_no=${item.order_no}`
})
}
function goShopping() {
uni.switchTab({ url: '/pages/index/index' })
}
async function doCancelOrder(item) {
uni.showModal({
title: '确认取消',
content: '确定要取消这个订单吗?',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '取消中...' })
try {
await cancelOrderApi(item.id, '用户主动取消')
uni.hideLoading()
uni.showToast({ title: '订单已取消', icon: 'success' })
// 刷新订单列表
fetchOrders(false)
} catch (e) {
uni.hideLoading()
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
}
}
}
})
}
async function payOrder(item) {
const openid = uni.getStorageSync('openid')
if (!openid) {
uni.showToast({ title: '缺少OpenID请重新登录', icon: 'none' })
return
}
if (!item || !item.order_no) return
uni.showLoading({ title: '拉起支付...' })
try {
const payRes = await createWechatOrder({ openid, order_no: item.order_no })
await 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
})
})
uni.hideLoading()
uni.showToast({ title: '支付成功', icon: 'success' })
navigateToGame(item)
} 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 navigateToGame(item) {
const playType = item.play_type
const activityId = item.activity_id
if (!activityId) {
fetchOrders(false) // 刷新订单列表
return
}
let url = ''
if (playType === 'match') {
url = `/pages-activity/activity/duiduipeng/index?activity_id=${activityId}`
} else if (playType === 'ichiban') {
url = `/pages-activity/activity/yifanshang/index?activity_id=${activityId}`
} else if (playType === 'infinity') {
url = `/pages-activity/activity/wuxianshang/index?activity_id=${activityId}`
}
if (url) {
uni.navigateTo({ url })
} else {
fetchOrders(false)
}
}
onLoad((opts) => {
const s = (opts && opts.status) || ''
if (s === 'completed' || s === 'pending') currentTab.value = s
fetchOrders(false)
})
onReachBottom(() => {
fetchOrders(true)
})
</script>
<style lang="scss" scoped>
/* ============================================
柯大鸭潮玩 - 订单页面
采用暖橙色调的订单列表设计
============================================ */
.page-container {
min-height: 100vh;
background: $bg-page;
position: relative;
padding-bottom: calc(40rpx + env(safe-area-inset-top) + env(safe-area-inset-bottom));
overflow: hidden;
}
/* 顶部 Tab */
.tabs {
@extend .glass-card;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
border-radius: 0;
border-top: none;
border-left: none;
border-right: none;
}
.tab-item {
position: relative;
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&.active {
.tab-text {
color: $brand-primary;
font-weight: 700;
font-size: 30rpx;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: $brand-primary;
border-radius: 4rpx;
}
}
}
.tab-text {
font-size: 28rpx;
color: $text-sub;
transition: all 0.3s;
}
.page-content {
position: relative;
z-index: 1;
padding: $spacing-lg;
padding-top: calc(88rpx + $spacing-lg); /* tabs height + spacing */
}
/* 错误提示 */
.error-toast {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
padding: $spacing-md $spacing-lg;
background: rgba($uni-color-error, 0.1);
border-radius: $radius-lg;
margin-bottom: $spacing-lg;
.error-icon { font-size: $font-lg; }
.error-text { color: $uni-color-error; font-size: $font-sm; }
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.loading-text {
margin-top: $spacing-lg;
color: $text-sub;
font-size: $font-sm;
}
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba($brand-primary, 0.2);
border-top-color: $brand-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
&.small {
width: 32rpx;
height: 32rpx;
border-width: 3rpx;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx $spacing-xl;
animation: fadeInUp 0.5s ease-out;
}
.empty-illustration {
width: 200rpx;
height: 200rpx;
background: rgba($brand-primary, 0.05);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-xl;
}
.empty-icon {
font-size: 80rpx;
animation: float 3s ease-in-out infinite;
}
.empty-title {
font-size: $font-xl;
font-weight: 700;
color: $text-main;
margin-bottom: $spacing-sm;
}
.empty-desc {
font-size: $font-sm;
color: $text-sub;
margin-bottom: $spacing-xl;
}
.empty-btn {
background: $gradient-brand !important;
color: #fff !important;
border: none !important;
padding: 0 60rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: $radius-round;
font-size: $font-md;
font-weight: 600;
box-shadow: $shadow-warm;
}
/* 订单列表 */
.orders-list {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
/* 订单卡片 */
.order-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10rpx);
border-radius: $radius-xl;
overflow: hidden;
box-shadow: $shadow-sm;
animation: fadeInUp 0.4s ease-out backwards;
animation-delay: var(--delay, 0s);
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.6);
&:active {
transform: scale(0.98);
box-shadow: none;
}
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-md $spacing-lg;
border-bottom: 1rpx solid $border-color-light;
background: rgba($bg-secondary, 0.3);
}
.order-type {
display: flex;
align-items: center;
gap: $spacing-xs;
}
.type-icon {
font-size: $font-lg;
}
.type-name {
font-size: $font-sm;
color: $text-sub;
font-weight: 500;
}
.order-status {
font-size: $font-xs;
padding: 6rpx 16rpx;
border-radius: $radius-round;
font-weight: 600;
&.status-pending {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
&.status-success {
background: rgba($uni-color-success, 0.1);
color: $uni-color-success;
}
&.status-normal {
background: rgba($text-sub, 0.1);
color: $text-sub;
}
&.status-cancelled {
background: rgba($text-placeholder, 0.1);
color: $text-placeholder;
}
&.status-processing {
background: rgba($accent-gold, 0.15);
color: #B45309;
}
}
/* 订单内容 */
.order-body {
display: flex;
padding: $spacing-lg;
gap: $spacing-lg;
}
.product-image-wrap {
width: 160rpx;
height: 160rpx;
border-radius: $radius-lg;
overflow: hidden;
position: relative;
flex-shrink: 0;
background: $bg-secondary;
}
.product-image {
width: 100%;
height: 100%;
}
.image-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.winner-badge {
color: #fff;
font-size: $font-sm;
font-weight: 700;
background: $gradient-gold;
padding: 6rpx 16rpx;
border-radius: $radius-sm;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}
.product-title {
font-size: $font-md;
font-weight: 700;
color: $text-main;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-meta {
display: flex;
flex-wrap: wrap;
gap: $spacing-sm;
margin-top: $spacing-xs;
}
.meta-item {
font-size: $font-xs;
color: $text-sub;
background: $bg-secondary;
padding: 4rpx 12rpx;
border-radius: $radius-sm;
.coupon-tag {
color: #FF6B6B;
background: rgba(255, 107, 107, 0.1);
}
.card-tag {
color: #6C5CE7;
background: rgba(108, 92, 231, 0.1);
}
}
.order-time {
font-size: $font-xs;
color: $text-placeholder;
margin-top: auto;
}
/* 订单底部 */
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-md $spacing-lg;
background: rgba($bg-secondary, 0.3);
border-top: 1rpx solid $border-color-light;
}
.order-no {
display: flex;
align-items: center;
gap: $spacing-xs;
}
.no-label {
font-size: $font-xs;
color: $text-placeholder;
}
.no-value {
font-size: $font-xs;
color: $text-sub;
font-family: 'SF Mono', monospace;
}
.order-amount {
display: flex;
align-items: baseline;
gap: $spacing-xs;
}
.amount-label {
font-size: $font-xs;
color: $text-sub;
}
.amount-value {
font-size: $font-lg;
font-weight: 800;
color: $brand-primary;
font-family: 'DIN Alternate', sans-serif;
}
/* 操作按钮 */
.order-actions {
display: flex;
justify-content: flex-end;
gap: $spacing-md;
padding: $spacing-md $spacing-lg;
border-top: 1rpx solid $border-color-light;
}
.action-btn {
height: 64rpx;
line-height: 64rpx;
padding: 0 32rpx;
border-radius: $radius-round;
font-size: $font-sm;
font-weight: 600;
&.primary {
background: $gradient-brand !important;
color: #fff !important;
border: none !important;
}
&.secondary {
background: transparent !important;
color: $text-sub !important;
border: 2rpx solid $border-color !important;
}
}
/* 加载更多 */
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
padding: $spacing-xl 0;
color: $text-sub;
font-size: $font-sm;
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-md;
padding: $spacing-xl 0;
color: $text-placeholder;
font-size: $font-xs;
.divider {
width: 60rpx;
height: 1rpx;
background: $border-color-light;
}
}
/* 动画 */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
</style>