Files
medical-mall/pages/mall/consumer/orderss.uvue

1051 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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="orders-page">
<!-- 顶部栏 -->
<!-- <view class="orders-header" @click="goBackToHome">
<view class="header-left">
<text class="header-title">我的订单</text>
</view>
<view class="header-right">
<text class="search-icon" @click.stop="navigateToSearch">🔍</text>
</view>
</view> -->
<!-- 订单状态标签页 -->
<view class="order-tabs">
<view :class="['order-tab', { active: activeTab === 'all' }]" @click="changeTab('all')">
<text class="tab-text">全部</text>
</view>
<view :class="['order-tab', { active: activeTab === 'pending' }]" @click="changeTab('pending')">
<text class="tab-text">待支付</text>
<text v-if="tabBadges.pending > 0" class="tab-badge">{{ tabBadges.pending }}</text>
</view>
<view :class="['order-tab', { active: activeTab === 'shipping' }]" @click="changeTab('shipping')">
<text class="tab-text">待发货</text>
<text v-if="tabBadges.shipping > 0" class="tab-badge">{{ tabBadges.shipping }}</text>
</view>
<view :class="['order-tab', { active: activeTab === 'receiving' }]" @click="changeTab('receiving')">
<text class="tab-text">待收货</text>
<text v-if="tabBadges.receiving > 0" class="tab-badge">{{ tabBadges.receiving }}</text>
</view>
<view :class="['order-tab', { active: activeTab === 'review' }]" @click="changeTab('review')">
<text class="tab-text">待评价</text>
<text v-if="tabBadges.review > 0" class="tab-badge">{{ tabBadges.review }}</text>
</view>
</view>
<!-- 订单列表 -->
<scroll-view class="orders-list" scroll-y @scrolltolower="loadMore">
<!-- 订单为空 -->
<view v-if="orders.length === 0 && !isLoading" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无订单</text>
<text class="empty-subtext">去逛逛吧,有惊喜在等你</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单项 -->
<view v-for="order in orders" :key="order.id" class="order-item">
<!-- 订单头部 -->
<view class="order-header">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- 订单商品 -->
<view class="order-products" @click="viewOrderDetail(order)">
<view v-for="(item, index) in order.items" :key="index" class="product-item">
<image class="product-image" :src="item.product_image || '/static/default-product.png'" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">
{{ getSpecText(item.sku_specifications) }}
</text>
<view class="product-price-row">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<text class="order-time">{{ formatTime(order.created_at) }}</text>
<view class="order-total">
共{{ getTotalQuantity(order.items) }}件商品 合计:
<text class="total-amount">¥{{ order.actual_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<button v-if="order.status === 1"
class="action-btn pay"
@click="payOrder(order)">
立即支付
</button>
<button v-if="order.status === 1"
class="action-btn cancel"
@click="cancelOrder(order)">
取消订单
</button>
<button v-if="order.status === 2"
class="action-btn remind"
@click="remindShipment(order)">
提醒发货
</button>
<button v-if="order.status === 3"
class="action-btn confirm"
@click="confirmReceipt(order)">
确认收货
</button>
<button v-if="order.status === 4"
class="action-btn review"
@click="goToReview(order)">
评价
</button>
<button v-if="order.status === 4 || order.status === 5"
class="action-btn delete"
@click="deleteOrder(order)">
删除订单
</button>
<button v-if="order.status === 4"
class="action-btn rebuy"
@click="reBuy(order)">
再次购买
</button>
<button class="action-btn service" @click="contactService(order)">
联系客服
</button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text class="no-more-text">没有更多订单了</text>
</view>
</scroll-view>
<!-- 底部导航 -->
<view class="bottom-navigation">
<view class="nav-item" @click="goToHome">
<text class="nav-icon">🏠</text>
<text class="nav-text">首页</text>
</view>
<view class="nav-item" @click="goToCategory">
<text class="nav-icon">📂</text>
<text class="nav-text">分类</text>
</view>
<view class="nav-item active">
<text class="nav-icon">📦</text>
<text class="nav-text">订单</text>
</view>
<view class="nav-item" @click="goToProfile">
<text class="nav-icon">👤</text>
<text class="nav-text">我的</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import { onShow, onBackPress } from '@dcloudio/uni-app'
import type { OrderType } from '@/types/mall-types.uts'
// import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
id: string
product_id: string
product_name: string
product_image: string
sku_specifications: any
price: number
quantity: number
}
type FullOrderType = OrderType & {
items: Array<OrderItemType>
}
type TabBadgesType = {
pending: number
shipping: number
receiving: number
review: number
}
const activeTab = ref<string>('all')
const orders = ref<Array<FullOrderType>>([])
const tabBadges = ref<TabBadgesType>({
pending: 0,
shipping: 0,
receiving: 0,
review: 0
})
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(10)
const hasMore = ref<boolean>(true)
// 监听标签页变化
watch(activeTab, () => {
loadOrders()
})
// 生命周期
onMounted(() => {
loadOrderCounts()
loadOrders()
})
// 加载订单数量统计
const loadOrderCounts = async () => {
const userId = getCurrentUserId() || 'user_001'
if (!userId) return
try {
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 过滤当前用户的订单
const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 待支付
const pendingCount = userOrders.filter((o: any) => o.status === 1).length
tabBadges.value.pending = pendingCount
// 待发货
const shippingCount = userOrders.filter((o: any) => o.status === 2).length
tabBadges.value.shipping = shippingCount
// 待收货
const receivingCount = userOrders.filter((o: any) => o.status === 3).length
tabBadges.value.receiving = receivingCount
// 待评价
const reviewCount = userOrders.filter((o: any) => o.status === 4).length
tabBadges.value.review = reviewCount
} catch (err) {
console.error('加载订单统计异常:', err)
}
}
// 加载订单列表
const loadOrders = async (loadMore: boolean = false) => {
const userId = getCurrentUserId() || 'user_001' // 默认用户ID
if (!userId) {
// uni.showToast({
// title: '请先登录',
// icon: 'none'
// })
// uni.navigateTo({
// url: '/pages/user/login'
// })
// 模拟已登录,用于测试
// return
}
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const page = loadMore ? currentPage.value + 1 : 1
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 过滤当前用户的订单
const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 根据标签页过滤
let filteredOrders = userOrders
switch (activeTab.value) {
case 'pending':
filteredOrders = userOrders.filter((o: any) => o.status === 1)
break
case 'shipping':
filteredOrders = userOrders.filter((o: any) => o.status === 2)
break
case 'receiving':
filteredOrders = userOrders.filter((o: any) => o.status === 3)
break
case 'review':
filteredOrders = userOrders.filter((o: any) => o.status === 4)
break
}
// 按时间倒序
filteredOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
// 分页 (模拟)
const total = filteredOrders.length
const start = (page - 1) * pageSize.value
const end = start + pageSize.value
const pagedOrders = filteredOrders.slice(start, end)
// 处理订单数据,补充商品图片等信息
const processedOrders = await processOrderData(pagedOrders)
if (loadMore) {
for (let i = 0; i < processedOrders.length; i++) {
orders.value.push(processedOrders[i])
}
currentPage.value = page
} else {
orders.value = processedOrders
currentPage.value = 1
}
hasMore.value = end < total
} catch (err) {
console.error('加载订单异常:', err)
} finally {
isLoading.value = false
}
}
// 处理订单数据
const processOrderData = async (orders: any[]): Promise<FullOrderType[]> => {
const processed: FullOrderType[] = []
for (let i = 0; i < orders.length; i++) {
const order = orders[i]
const orderItems = order.items || [] // 这里的 items 已经在 checkout 存入时有了
// 为每个商品项加载图片
// const itemsWithImages = await Promise.all(
// orderItems.map(async (item: any) => {
// const productImage = await getProductImage(item.product_id)
// return {
// ...item,
// product_image: productImage
// }
// })
// )
// 本地存储的 items 应该已经有 image 字段了,如果没有,再尝试获取
const itemsWithImages = orderItems.map((item: any) => {
return {
...item,
product_image: item.product_image || item.image || '/static/default-product.png',
product_name: item.product_name || item.name // 兼容不同字段名
}
})
processed.push({
...order,
items: itemsWithImages
})
}
return processed
}
// 获取商品图片
const getProductImage = async (productId: string): Promise<string> => {
try {
const { data, error } = await supa
.from('products')
.select('images')
.eq('id', productId)
.single()
if (error !== null) {
console.error('获取商品图片失败:', error)
return '/static/default-product.png'
}
return data?.images?.[0] || '/static/default-product.png'
} catch (err) {
console.error('获取商品图片异常:', err)
return '/static/default-product.png'
}
}
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null
}
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待支付',
2: '待发货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
}
// 获取状态样式类
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-shipping',
3: 'status-receiving',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
}
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
.join('; ')
}
return String(specs)
}
// 格式化时间
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 计算商品总数
const getTotalQuantity = (items: OrderItemType[]): number => {
return items.reduce((total, item) => total + item.quantity, 0)
}
// 返回主页
const goBackToHome = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
// 导航到搜索页面
const navigateToSearch = () => {
uni.navigateTo({
url: '/pages/mall/consumer/search'
})
}
// 监听手机返回键
onBackPress(() => {
goBackToHome()
return true // 阻止默认返回行为
})
// 标签页切换
const changeTab = (tab: string) => {
activeTab.value = tab
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadOrders(true)
}
}
// 查看订单详情
const viewOrderDetail = (order: FullOrderType) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${order.id}`
})
}
// 支付订单
const payOrder = (order: FullOrderType) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${order.actual_amount}`
})
}
// 取消订单
const cancelOrder = async (order: FullOrderType) => {
uni.showModal({
title: '取消订单',
content: '确定要取消这个订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('orders')
.update({ status: 5 })
.eq('id', order.id)
if (error !== null) {
console.error('取消订单失败:', error)
uni.showToast({
title: '取消失败',
icon: 'none'
})
return
}
order.status = 5
loadOrderCounts() // 重新加载统计
uni.showToast({
title: '订单已取消',
icon: 'success'
})
} catch (err) {
console.error('取消订单异常:', err)
}
}
}
})
}
// 提醒发货
const remindShipment = (order: FullOrderType) => {
uni.showToast({
title: '已提醒商家发货',
icon: 'success'
})
}
// 确认收货
const confirmReceipt = async (order: FullOrderType) => {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('orders')
.update({ status: 4 })
.eq('id', order.id)
if (error !== null) {
console.error('确认收货失败:', error)
uni.showToast({
title: '确认失败',
icon: 'none'
})
return
}
order.status = 4
loadOrderCounts() // 重新加载统计
uni.showToast({
title: '确认收货成功',
icon: 'success'
})
} catch (err) {
console.error('确认收货异常:', err)
}
}
}
})
}
// 去评价
const goToReview = (order: FullOrderType) => {
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}`
})
}
// 删除订单
const deleteOrder = async (order: FullOrderType) => {
uni.showModal({
title: '删除订单',
content: '确定要删除这个订单吗?删除后将无法恢复',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('orders')
.delete()
.eq('id', order.id)
if (error !== null) {
console.error('删除订单失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
const index = orders.value.findIndex(o => o.id === order.id)
if (index !== -1) {
orders.value.splice(index, 1)
orders.value = [...orders.value]
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('删除订单异常:', err)
}
}
}
})
}
// 再次购买
const reBuy = async (order: FullOrderType) => {
// 将订单商品加入购物车
const userId = getCurrentUserId()
if (!userId) return
try {
const addPromises = order.items.map(item =>
supa
.from('shopping_cart')
.upsert({
user_id: userId,
product_id: item.product_id,
sku_id: null, // 这里需要获取SKU ID
quantity: item.quantity
})
)
await Promise.all(addPromises)
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
} catch (err) {
console.error('再次购买异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
// 联系客服
const contactService = (order: FullOrderType) => {
uni.navigateTo({
url: `/pages/mall/service/chat?orderId=${order.id}`
})
}
// 去逛逛
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
// 导航方法
const goToHome = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
const goToCategory = () => {
uni.switchTab({
url: '/pages/mall/consumer/category'
})
}
const goToProfile = () => {
uni.switchTab({
url: '/pages/mall/consumer/profile'
})
}
</script>
<style scoped>
.orders-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.orders-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.header-left {
flex: 1;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-right {
display: flex;
align-items: center;
}
.search-icon {
font-size: 20px;
color: #333333;
padding: 5px;
}
.order-tabs {
background-color: #ffffff;
display: flex;
flex-direction: row;
border-bottom: 1px solid #e5e5e5;
justify-content: space-between;
}
.order-tab {
flex: 1;
padding: 15px 0;
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.order-tab.active {
color: #007aff;
border-bottom: 2px solid #007aff;
}
.tab-text {
font-size: 14px;
color: #666666;
}
.order-tab.active .tab-text {
color: #007aff;
font-weight: bold;
}
.tab-badge {
position: absolute;
top: 8px;
right: 5px;
background-color: #ff4757;
color: #ffffff;
font-size: 10px;
padding: 2px 5px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
.orders-list {
flex: 1;
}
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.order-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #333333;
}
.order-status {
font-size: 14px;
padding: 4px 10px;
border-radius: 12px;
}
.status-pending {
background-color: #ffa726;
color: #ffffff;
}
.status-shipping {
background-color: #2196f3;
color: #ffffff;
}
.status-receiving {
background-color: #9c27b0;
color: #ffffff;
}
.status-completed {
background-color: #4caf50;
color: #ffffff;
}
.status-cancelled {
background-color: #9e9e9e;
color: #ffffff;
}
.order-products {
margin-bottom: 15px;
}
.product-item {
display: flex;
margin-bottom: 10px;
}
.product-item:last-child {
margin-bottom: 0;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 5px;
margin-right: 10px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 13px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.product-spec {
font-size: 12px;
color: #999999;
margin-bottom: 5px;
}
.product-price-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 14px;
color: #ff4757;
font-weight: bold;
}
.product-quantity {
font-size: 12px;
color: #666666;
}
.order-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-top: 10px;
border-top: 1px solid #f5f5f5;
}
.order-time {
font-size: 12px;
color: #999999;
}
.order-total {
font-size: 14px;
color: #333333;
}
.total-amount {
font-size: 16px;
color: #ff4757;
font-weight: bold;
margin-left: 5px;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.action-btn {
padding: 8px 15px;
border-radius: 15px;
font-size: 12px;
border: 1px solid;
background-color: #ffffff;
}
.action-btn.pay {
border-color: #ff4757;
color: #ff4757;
}
.action-btn.cancel {
border-color: #666666;
color: #666666;
}
.action-btn.remind {
border-color: #2196f3;
color: #2196f3;
}
.action-btn.confirm {
border-color: #4caf50;
color: #4caf50;
}
.action-btn.review {
border-color: #ffa726;
color: #ffa726;
}
.action-btn.delete {
border-color: #9e9e9e;
color: #9e9e9e;
}
.action-btn.rebuy {
border-color: #007aff;
color: #007aff;
}
.action-btn.service {
border-color: #9c27b0;
color: #9c27b0;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
background-color: #ffffff;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
/* 底部导航栏 */
.bottom-navigation {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #ffffff;
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid #f0f0f0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.nav-icon {
font-size: 24px;
margin-bottom: 2px;
}
.nav-text {
font-size: 10px;
color: #999;
}
.nav-item.active .nav-text {
color: #4CAF50;
font-weight: 500;
}
</style>