Files
jurong_circle_frontdesk/src/views/Orders.vue

905 lines
20 KiB
Vue
Raw Normal View History

2025-07-26 15:35:53 +08:00
<template>
<div class="orders-page">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-center">
<h1 class="nav-title">我的订单</h1>
</div>
<div class="nav-right">
<el-button
type="text"
@click="$router.push('/shop')"
class="shop-btn"
>
<el-icon><ShoppingBag /></el-icon>
商城
</el-button>
</div>
</nav>
<!-- 订单状态筛选 -->
<div class="filter-tabs">
<div class="tabs-container">
<div
v-for="tab in statusTabs"
:key="tab.value"
:class="['tab-item', { active: selectedStatus === tab.value }]"
@click="selectStatus(tab.value)"
>
<span>{{ tab.label }}</span>
<el-badge
v-if="tab.count > 0"
:value="tab.count"
class="tab-badge"
/>
</div>
</div>
</div>
<!-- 订单列表 -->
<div class="orders-content">
<div v-loading="loading" class="orders-list">
<div v-if="filteredOrders.length === 0" class="empty-state">
<el-icon size="60"><Box /></el-icon>
<p>{{ getEmptyText() }}</p>
<el-button type="primary" @click="$router.push('/shop')">
去购物
</el-button>
</div>
<div v-else>
<div v-for="order in filteredOrders" :key="order.id" class="order-card">
<!-- 订单头部 -->
<div class="order-header">
<div class="order-info">
<span class="order-number">订单号{{ order.orderNumber }}</span>
<span class="order-date">{{ formatDate(order.createdAt) }}</span>
</div>
<div class="order-status">
<el-tag :type="getStatusType(order.status)">{{ getStatusText(order.status) }}</el-tag>
</div>
</div>
<!-- 订单商品 -->
<div class="order-items">
<div
v-for="item in order.items"
:key="item.id"
class="order-item"
@click="goToProduct(item.productId)"
>
<img :src="item.product.image" :alt="item.product.name" class="item-image" />
<div class="item-info">
<h4 class="item-name">{{ item.product.name }}</h4>
<p class="item-desc">{{ truncateText(item.product.description, 40) }}</p>
<div class="item-price">
<span class="price">
<el-icon><Coin /></el-icon>
{{ item.points }}
</span>
<span class="quantity">x{{ item.quantity }}</span>
</div>
</div>
</div>
</div>
<!-- 订单总计 -->
<div class="order-total">
<div class="total-info">
<span>{{ order.totalQuantity }}件商品</span>
<span class="total-points">
总计<el-icon><Coin /></el-icon>{{ order.totalPoints }}
</span>
</div>
</div>
<!-- 订单操作 -->
<div class="order-actions">
<el-button
v-if="order.status === 'pending'"
size="small"
@click="cancelOrder(order.id)"
>
取消订单
</el-button>
<el-button
v-if="order.status === 'shipped'"
type="primary"
size="small"
@click="confirmReceive(order.id)"
>
确认收货
</el-button>
<el-button
v-if="order.status === 'completed'"
size="small"
@click="showReviewDialog(order)"
>
评价
</el-button>
<el-button
size="small"
@click="viewOrderDetail(order.id)"
>
查看详情
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="load-more">
<el-button @click="loadMore" :loading="loadingMore">
加载更多
</el-button>
</div>
</div>
<!-- 评价对话框 -->
<el-dialog
v-model="showReview"
title="商品评价"
width="90%"
:before-close="handleReviewClose"
>
<div v-if="reviewOrder" class="review-form">
<div v-for="item in reviewOrder.items" :key="item.id" class="review-item">
<div class="review-product">
<img :src="item.product.image" :alt="item.product.name" class="product-image" />
<div class="product-info">
<h4>{{ item.product.name }}</h4>
<p>{{ item.product.description }}</p>
</div>
</div>
<div class="review-rating">
<span class="rating-label">评分</span>
<el-rate v-model="item.rating" size="large" />
</div>
<div class="review-content">
<el-input
v-model="item.reviewContent"
type="textarea"
:rows="3"
placeholder="请分享您的使用体验..."
maxlength="200"
show-word-limit
/>
</div>
<div class="review-images">
<el-upload
v-model:file-list="item.reviewImages"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="3"
>
<el-icon><Plus /></el-icon>
</el-upload>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showReview = false">取消</el-button>
<el-button type="primary" @click="submitReview" :loading="submittingReview">
提交评价
</el-button>
</span>
</template>
</el-dialog>
<!-- 订单详情对话框 -->
<el-dialog
v-model="showOrderDetail"
title="订单详情"
width="90%"
>
<div v-if="orderDetail" class="order-detail">
<div class="detail-section">
<h4>订单信息</h4>
<div class="detail-item">
<span class="label">订单号</span>
<span class="value">{{ orderDetail.orderNumber }}</span>
</div>
<div class="detail-item">
<span class="label">下单时间</span>
<span class="value">{{ formatDateTime(orderDetail.createdAt) }}</span>
</div>
<div class="detail-item">
<span class="label">订单状态</span>
<span class="value">
<el-tag :type="getStatusType(orderDetail.status)">
{{ getStatusText(orderDetail.status) }}
</el-tag>
</span>
</div>
</div>
<div class="detail-section">
<h4>商品信息</h4>
<div v-for="item in orderDetail.items" :key="item.id" class="detail-product">
<img :src="item.product.image" :alt="item.product.name" />
<div class="product-info">
<h5>{{ item.product.name }}</h5>
<p>{{ item.product.description }}</p>
<div class="product-price">
<span><el-icon><Coin /></el-icon>{{ item.points }} x {{ item.quantity }}</span>
</div>
</div>
</div>
</div>
<div class="detail-section">
<h4>配送信息</h4>
<div class="detail-item">
<span class="label">收货地址</span>
<span class="value">{{ orderDetail.shippingAddress || '虚拟商品,无需配送' }}</span>
</div>
<div class="detail-item">
<span class="label">物流信息</span>
<span class="value">{{ orderDetail.trackingNumber || '暂无' }}</span>
</div>
</div>
<div class="detail-section">
<h4>费用明细</h4>
<div class="detail-item">
<span class="label">商品总计</span>
<span class="value"><el-icon><Coin /></el-icon>{{ orderDetail.totalPoints }}</span>
</div>
<div class="detail-item total">
<span class="label">实付积分</span>
<span class="value"><el-icon><Coin /></el-icon>{{ orderDetail.totalPoints }}</span>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
ArrowLeft,
ShoppingBag,
Box,
Coin,
Plus
} from '@element-plus/icons-vue'
import api from '@/utils/api'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const loadingMore = ref(false)
const selectedStatus = ref('all')
const orders = ref([])
const page = ref(1)
const hasMore = ref(true)
const showReview = ref(false)
const showOrderDetail = ref(false)
const reviewOrder = ref(null)
const orderDetail = ref(null)
const submittingReview = ref(false)
// 状态标签
const statusTabs = ref([
{ label: '全部', value: 'all', count: 0 },
{ label: '待发货', value: 'pending', count: 0 },
{ label: '已发货', value: 'shipped', count: 0 },
{ label: '已完成', value: 'completed', count: 0 },
{ label: '已取消', value: 'cancelled', count: 0 }
])
// 计算属性
const filteredOrders = computed(() => {
if (selectedStatus.value === 'all') {
return orders.value
}
return orders.value.filter(order => order.status === selectedStatus.value)
})
// 方法
const selectStatus = (status) => {
selectedStatus.value = status
}
const getEmptyText = () => {
const textMap = {
all: '暂无订单',
pending: '暂无待发货订单',
shipped: '暂无已发货订单',
completed: '暂无已完成订单',
cancelled: '暂无已取消订单'
}
return textMap[selectedStatus.value]
}
const getStatusType = (status) => {
const typeMap = {
pending: 'warning',
shipped: 'primary',
completed: 'success',
cancelled: 'danger'
}
return typeMap[status] || 'info'
}
const getStatusText = (status) => {
const textMap = {
pending: '待发货',
shipped: '已发货',
completed: '已完成',
cancelled: '已取消'
}
return textMap[status] || '未知状态'
}
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN')
}
const formatDateTime = (date) => {
return new Date(date).toLocaleString('zh-CN')
}
const truncateText = (text, maxLength) => {
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
const goToProduct = (productId) => {
router.push(`/product/${productId}`)
}
const cancelOrder = async (orderId) => {
try {
await ElMessageBox.confirm('确定要取消这个订单吗?', '确认取消', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await api.put(`/orders/${orderId}/cancel`)
// 更新订单状态
const order = orders.value.find(o => o.id === orderId)
if (order) {
order.status = 'cancelled'
}
updateStatusCounts()
ElMessage.success('订单已取消')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('取消订单失败')
}
}
}
const confirmReceive = async (orderId) => {
try {
await ElMessageBox.confirm('确认已收到商品吗?', '确认收货', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await api.put(`/orders/${orderId}/receive`)
// 更新订单状态
const order = orders.value.find(o => o.id === orderId)
if (order) {
order.status = 'completed'
}
updateStatusCounts()
ElMessage.success('确认收货成功')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('确认收货失败')
}
}
}
const showReviewDialog = (order) => {
reviewOrder.value = {
...order,
items: order.items.map(item => ({
...item,
rating: 5,
reviewContent: '',
reviewImages: []
}))
}
showReview.value = true
}
const handleReviewClose = () => {
reviewOrder.value = null
showReview.value = false
}
const submitReview = async () => {
try {
submittingReview.value = true
const reviewData = {
orderId: reviewOrder.value.id,
reviews: reviewOrder.value.items.map(item => ({
productId: item.productId,
rating: item.rating,
content: item.reviewContent,
images: item.reviewImages.map(img => img.url)
}))
}
await api.post('/reviews', reviewData)
showReview.value = false
ElMessage.success('评价提交成功')
} catch (error) {
ElMessage.error('评价提交失败')
} finally {
submittingReview.value = false
}
}
const viewOrderDetail = async (orderId) => {
try {
const response = await api.get(`/orders/${orderId}`)
orderDetail.value = response.data
showOrderDetail.value = true
} catch (error) {
ElMessage.error('获取订单详情失败')
}
}
const getOrders = async (isLoadMore = false) => {
try {
if (!isLoadMore) {
loading.value = true
page.value = 1
} else {
loadingMore.value = true
}
2025-08-01 09:33:46 +08:00
const {data} = await api.get('/orders', {
2025-07-26 15:35:53 +08:00
params: {
page: page.value,
limit: 10
}
})
2025-08-01 09:33:46 +08:00
console.log(data,'response');
2025-07-26 15:35:53 +08:00
if (isLoadMore) {
2025-08-01 09:33:46 +08:00
orders.value.push(...data.data.orders)
2025-07-26 15:35:53 +08:00
} else {
2025-08-01 09:33:46 +08:00
orders.value = data.data.orders
2025-07-26 15:35:53 +08:00
}
2025-08-01 09:33:46 +08:00
hasMore.value = data.data.hasMore
2025-07-26 15:35:53 +08:00
page.value++
updateStatusCounts()
} catch (error) {
ElMessage.error('获取订单列表失败')
} finally {
loading.value = false
loadingMore.value = false
}
}
const loadMore = () => {
getOrders(true)
}
const updateStatusCounts = () => {
const counts = {
all: orders.value.length,
pending: 0,
shipped: 0,
completed: 0,
cancelled: 0
}
orders.value.forEach(order => {
counts[order.status]++
})
statusTabs.value.forEach(tab => {
tab.count = counts[tab.value]
})
}
// 生命周期
onMounted(() => {
getOrders()
})
</script>
<style scoped>
.orders-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
background: white;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-left,
.nav-right {
flex: 1;
}
.nav-right {
display: flex;
justify-content: flex-end;
}
.back-btn,
.shop-btn {
color: #409eff;
font-size: 14px;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #333;
}
.filter-tabs {
background: white;
border-bottom: 1px solid #eee;
padding: 0 16px;
}
.tabs-container {
display: flex;
overflow-x: auto;
}
.tab-item {
position: relative;
padding: 16px 20px;
color: #666;
font-size: 14px;
cursor: pointer;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab-item.active {
color: #409eff;
border-bottom-color: #409eff;
}
.tab-badge {
position: absolute;
top: 8px;
right: 8px;
}
.orders-content {
padding: 16px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.orders-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.order-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
}
.order-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.order-number {
font-size: 14px;
color: #333;
font-weight: 500;
}
.order-date {
font-size: 12px;
color: #999;
}
.order-items {
padding: 16px;
}
.order-item {
display: flex;
gap: 12px;
padding: 8px 0;
cursor: pointer;
transition: all 0.3s;
}
.order-item:hover {
background: #f8f9fa;
border-radius: 8px;
padding: 8px;
margin: 0 -8px;
}
.item-image {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.item-name {
margin: 0;
font-size: 14px;
color: #333;
font-weight: 500;
line-height: 1.4;
}
.item-desc {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
}
.item-price {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
display: flex;
align-items: center;
gap: 2px;
color: #ff6b35;
font-weight: 600;
font-size: 14px;
}
.quantity {
color: #999;
font-size: 12px;
}
.order-total {
padding: 16px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.total-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-points {
display: flex;
align-items: center;
gap: 4px;
color: #ff6b35;
font-weight: 600;
font-size: 16px;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px;
}
.load-more {
text-align: center;
padding: 20px;
}
.review-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.review-item {
border: 1px solid #eee;
border-radius: 8px;
padding: 16px;
}
.review-product {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.review-product .product-image {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
}
.review-product .product-info h4 {
margin: 0 0 4px 0;
font-size: 14px;
color: #333;
}
.review-product .product-info p {
margin: 0;
font-size: 12px;
color: #666;
}
.review-rating {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.rating-label {
font-size: 14px;
color: #333;
}
.review-content {
margin-bottom: 16px;
}
.review-images {
margin-bottom: 16px;
}
.order-detail {
display: flex;
flex-direction: column;
gap: 24px;
}
.detail-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
}
.detail-item.total {
font-weight: 600;
color: #ff6b35;
border-bottom: none;
padding-top: 16px;
border-top: 1px solid #eee;
}
.detail-item .label {
color: #666;
font-size: 14px;
}
.detail-item .value {
color: #333;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.detail-product {
display: flex;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 8px;
}
.detail-product img {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
}
.detail-product .product-info h5 {
margin: 0 0 4px 0;
font-size: 14px;
color: #333;
}
.detail-product .product-info p {
margin: 0 0 8px 0;
font-size: 12px;
color: #666;
}
.detail-product .product-price {
display: flex;
align-items: center;
gap: 4px;
color: #ff6b35;
font-weight: 600;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 480px) {
.order-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.order-actions {
flex-wrap: wrap;
}
.detail-item {
flex-direction: column;
gap: 4px;
}
}
</style>