Files
medical-mall/pages/mall/consumer/orders.uvue
2026-02-02 17:34:31 +08:00

960 lines
24 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.
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header">
<view class="header-search full-width">
<input
class="search-input"
type="text"
placeholder="搜索订单号或商品名称"
:value="searchKeyword"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<text v-if="searchKeyword" class="search-clear" @click="clearSearch">×</text>
<text v-else class="search-icon">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs">
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-container">
<view
v-for="tab in orderTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 订单列表 -->
<scroll-view
scroll-y
class="orders-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="order-list">
<view
v-for="order in orders"
:key="order.id"
class="order-card"
>
<!-- 订单头部 -->
<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">
<view
v-for="product in order.products"
:key="product.id"
class="order-product"
@click="navigateToProduct(product)"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.spec }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">×{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="info-label">商品合计</text>
<text class="info-value">¥{{ order.product_amount }}</text>
</view>
<view class="info-row">
<text class="info-label">运费</text>
<text class="info-value">¥{{ order.shipping_fee }}</text>
</view>
<view class="info-row total">
<text class="info-label">实付款</text>
<text class="info-value total-price">¥{{ order.total_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
</view>
<!-- 安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
// 响应式数据
const orders = ref<any[]>([])
const allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
// 订单标签页
const orderTabs = reactive([
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
{ id: 'delivering', name: '待收货', count: 0 },
{ id: 'completed', name: '已完成', count: 0 },
{ id: 'cancelled', name: '已取消', count: 0 }
])
// Removed Mock Data
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
const targetStatus = statusMap[activeTab.value]
return orders.value.filter(order => order.status === targetStatus)
})
// 生命周期
onLoad((options) => {
if (options['status']) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type']) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
}
})
onShow(() => {
loadOrders()
})
// 加载订单数据
const loadOrders = async () => {
loading.value = true
try {
// Fetch all orders from Supabase (status=0)
const fetchedOrders = await supabaseService.getOrders(0)
// Map to View Model
const mappedOrders = fetchedOrders.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.order_status,
create_time: order.created_at,
product_amount: order.product_amount || order.actual_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.ml_order_items || []).map((item: any) => ({
id: item.product_id,
name: item.product_name,
price: item.price,
image: item.product_image,
spec: item.spec || '',
quantity: item.quantity
}))
}))
// Sort by created_at desc
mappedOrders.sort((a: any, b: any) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
return timeB - timeA
})
allOrdersList.value = mappedOrders
// Update tab counts
updateTabsCounts(mappedOrders)
// Apply current tab filter
filterOrdersByTab()
} catch (err) {
console.error('加载订单异常:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
loading.value = false
}
}
const updateTabsCounts = (allOrders: any[]) => {
orderTabs[0].count = allOrders.length
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length
}
const filterOrdersByTab = () => {
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else {
const targetStatus = statusMap[activeTab.value]
orders.value = allOrdersList.value.filter((o: any) => o.status === targetStatus)
}
}
const formatDate = (isoString: string): string => {
if (!isoString) return ''
const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 搜索相关函数
const onSearchInput = (e: any) => {
searchKeyword.value = e.detail.value
performSearch()
}
const onSearchConfirm = () => {
performSearch()
}
const clearSearch = () => {
searchKeyword.value = ''
performSearch()
}
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData() // 这里需要获取完整的订单数据
const filtered = allOrders.filter((order: any) => {
// 搜索订单号
if (order.order_no && order.order_no.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
if (order.products && Array.isArray(order.products)) {
return order.products.some((product: any) => {
return product.name && product.name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
const getCurrentOrderData = () => {
return allOrdersList.value
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
}
return String(specs)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
filterOrdersByTab()
}
// 获取状态文本
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-delivering',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
hasMore.value = false
}
// 订单操作函数
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '订单已取消',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 5
orders.value = [...orders.value]
}
}
}
})
}
const payOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId}`
})
}
const remindShipping = (orderId: string) => {
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
})
}
const viewLogistics = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
}
}
})
}
const goReview = (order: any) => {
const productIds = order.products.map((p: any) => p.id).join(',')
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}`
})
}
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
}
}
})
}
const viewOrderDetail = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}
</script>
<style>
.orders-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 头部 */
.orders-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.header-search.full-width {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.search-input {
flex: 1;
height: 36px;
border: 1px solid #ddd;
border-radius: 18px;
padding: 0 40px 0 16px;
font-size: 14px;
background-color: #f5f5f5;
color: #333;
width: 100%;
}
.search-input::placeholder {
color: #999;
font-size: 12px;
}
.search-input:focus {
outline: none;
border-color: #ff5000;
background-color: white;
}
.search-icon {
position: absolute;
right: 12px;
font-size: 18px;
color: #999;
}
.search-clear {
position: absolute;
right: 12px;
font-size: 20px;
color: #999;
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background-color: #ddd;
cursor: pointer;
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
z-index: 10;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-container {
display: flex;
flex-direction: row;
padding: 0 10px;
min-width: 100%;
}
.tab-item {
/* 移除 flex: 1改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
}
.tab-item.active {
color: #ff5000;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000;
}
.tab-name {
font-size: 14px;
}
.tab-count {
margin-left: 4px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
}
/* 空状态 */
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 订单列表 */
.order-list {
padding: 10px;
}
.order-card {
background-color: white;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #666;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled {
color: #999;
}
/* 订单商品 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
margin-bottom: 15px;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 15px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
.product-spec {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 14px;
color: #666;
}
/* 订单信息 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row.total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
/* 订单操作 */
.order-actions {
padding: 15px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
}
.action-btn.cancel {
color: #666;
border-color: #ccc;
}
.action-btn.pay {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.remind {
color: #666;
border-color: #ccc;
}
.action-btn.view {
color: #666;
border-color: #ccc;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
/* 加载更多 */
.loading-more {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;
font-size: 13px;
padding: 20px 0;
}
/* 安全区域 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;
margin-right: 5px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
}
@media screen and (min-width: 415px) {
.order-card {
margin-bottom: 15px;
}
}
</style>