809 lines
20 KiB
Vue
Raw Permalink 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.

<template>
<view class="page-container">
<!-- 顶部 Tab -->
<view class="tabs">
<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>
</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">实付</text>
<text class="amount-value">{{ formatAmount(item.actual_amount || item.total_amount) }}</text>
</view>
</view>
<!-- 快捷操作 -->
<view class="order-actions" v-if="currentTab === 'pending'">
<button class="action-btn secondary" @tap.stop="cancelOrder(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 } from '../../api/appUser'
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) {
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 getOrderTitle(item) {
// 优先使用 remark 中的商品名称
if (item.remark && !item.remark.startsWith('lottery:')) {
return item.remark
}
// 其次使用 items 中的商品名称
if (item.items && item.items.length > 0) {
return item.items[0].title || '商品'
}
// 使用活动名称
if (item.activity_name) {
return item.activity_name
}
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) return '🎰' // 抽奖订单
if (sourceType === 1) return '🛒' // 商城订单
return '📦'
}
function getTypeName(item) {
const sourceType = item.source_type
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
currentTab.value = tab
fetchOrders(false)
}
function apiStatus() {
return currentTab.value === 'pending' ? 'pending' : 'completed'
}
// 过滤掉 source_type=3 的发奖订单
function filterOrders(items) {
if (!Array.isArray(items)) return []
return items.filter(item => item.source_type !== 3)
}
async function fetchOrders(append) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!user_id || !token || !phoneBound) {
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/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' })
}
}
}
})
}
function payOrder(item) {
// TODO: 跳转支付
uni.showToast({ title: '支付功能开发中', icon: 'none' })
}
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-bottom));
}
/* 顶部 Tab - 与货柜页面保持一致 */
.tabs {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 88rpx;
background: rgba($bg-card, 0.95);
backdrop-filter: blur(20rpx);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.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: $bg-card;
border-radius: $radius-xl;
overflow: hidden;
box-shadow: $shadow-card;
animation: fadeInUp 0.4s ease-out backwards;
animation-delay: var(--delay, 0s);
transition: all 0.2s;
&:active {
transform: scale(0.98);
}
}
/* 订单头部 */
.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;
}
.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>