2025-12-20 20:41:30 +08:00
|
|
|
|
<template>
|
2025-12-30 21:06:32 +08:00
|
|
|
|
<UserLayout>
|
2025-12-20 20:41:30 +08:00
|
|
|
|
<div class="order-page">
|
|
|
|
|
|
<el-card class="order-card">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="order-header">
|
|
|
|
|
|
<span>我的订单</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="orderList.length === 0" class="empty-orders">
|
|
|
|
|
|
<el-empty description="暂无订单" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else>
|
|
|
|
|
|
<!-- 订单列表 -->
|
|
|
|
|
|
<div class="order-list">
|
|
|
|
|
|
<el-table :data="orderList" style="width: 100%" row-key="id">
|
2025-12-23 20:57:02 +08:00
|
|
|
|
<el-table-column prop="orderNo" label="订单号" min-width="200" />
|
2025-12-30 21:06:32 +08:00
|
|
|
|
<el-table-column
|
|
|
|
|
|
prop="createTime"
|
|
|
|
|
|
label="下单时间"
|
|
|
|
|
|
min-width="180"
|
|
|
|
|
|
>
|
2025-12-20 20:41:30 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
{{ formatDate(scope.row.createTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
prop="productCount"
|
|
|
|
|
|
label="商品数量"
|
2025-12-23 20:57:02 +08:00
|
|
|
|
min-width="100"
|
2025-12-20 20:41:30 +08:00
|
|
|
|
/>
|
2025-12-30 21:06:32 +08:00
|
|
|
|
<el-table-column
|
|
|
|
|
|
prop="totalAmount"
|
|
|
|
|
|
label="订单总额"
|
|
|
|
|
|
min-width="120"
|
|
|
|
|
|
>
|
2025-12-20 20:41:30 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
¥{{ scope.row.totalAmount.toFixed(2) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-30 21:06:32 +08:00
|
|
|
|
<el-table-column
|
|
|
|
|
|
prop="statusText"
|
|
|
|
|
|
label="订单状态"
|
|
|
|
|
|
min-width="100"
|
|
|
|
|
|
/>
|
2025-12-23 20:57:02 +08:00
|
|
|
|
<el-table-column label="操作" min-width="120">
|
2025-12-20 20:41:30 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
link
|
|
|
|
|
|
@click="viewOrderDetail(scope.row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看详情
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-model:current-page="currentPage"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:page-sizes="[5, 10, 20, 50]"
|
|
|
|
|
|
:total="total"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 订单详情模态框 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="dialogVisible"
|
|
|
|
|
|
title="订单详情"
|
|
|
|
|
|
width="900px"
|
|
|
|
|
|
class="order-detail-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="currentOrder" class="order-detail">
|
|
|
|
|
|
<h3 class="section-title">订单信息</h3>
|
|
|
|
|
|
<el-descriptions :column="2" border>
|
|
|
|
|
|
<el-descriptions-item label="订单号">
|
|
|
|
|
|
{{ currentOrder.orderNo }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="下单时间">
|
2025-12-21 02:21:26 +08:00
|
|
|
|
{{ formatDateTime(currentOrder.createTime) }}
|
2025-12-20 20:41:30 +08:00
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="订单状态">
|
|
|
|
|
|
{{ getOrderStatusText(currentOrder.status) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="支付方式">
|
|
|
|
|
|
{{ getPayTypeText(currentOrder.payType) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="订单总额">
|
|
|
|
|
|
¥{{ currentOrder.totalAmount.toFixed(2) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="实付金额">
|
|
|
|
|
|
¥{{ currentOrder.payAmount.toFixed(2) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="section-title">收货信息</h3>
|
|
|
|
|
|
<el-descriptions :column="1" border>
|
|
|
|
|
|
<el-descriptions-item label="收货人">
|
|
|
|
|
|
{{ currentOrder.receiverName }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="联系电话">
|
|
|
|
|
|
{{ currentOrder.receiverPhone }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="收货地址">
|
|
|
|
|
|
{{ currentOrder.receiverAddress }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="section-title">商品信息</h3>
|
|
|
|
|
|
<div class="order-items">
|
|
|
|
|
|
<el-table :data="currentOrder.orderItems" style="width: 100%">
|
|
|
|
|
|
<el-table-column prop="productImage" label="商品" width="300">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<div class="product-info">
|
|
|
|
|
|
<el-image
|
|
|
|
|
|
:src="scope.row.productImage"
|
|
|
|
|
|
class="product-image"
|
|
|
|
|
|
fit="cover"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="product-details">
|
|
|
|
|
|
<div class="product-name">
|
|
|
|
|
|
{{ scope.row.productName }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="product-sku">{{ scope.row.skuName }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="price" label="单价" width="168">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
¥{{ scope.row.price.toFixed(2) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="quantity" label="数量" width="200" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="totalPrice" label="小计" width="200">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
¥{{ scope.row.totalPrice.toFixed(2) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2025-12-30 21:06:32 +08:00
|
|
|
|
</UserLayout>
|
2025-12-20 20:41:30 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
2025-12-30 21:06:32 +08:00
|
|
|
|
import UserLayout from "@/layouts/UserLayout.vue";
|
2025-12-20 20:41:30 +08:00
|
|
|
|
import { ref, onMounted } from "vue";
|
|
|
|
|
|
import { useUserStore } from "@/stores/UserStore";
|
|
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
|
import { getOrderList, getOrderDetail } from "@/apis/order";
|
|
|
|
|
|
import type { OrderListItem, OrderDetail } from "@/types/order";
|
|
|
|
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
|
|
|
|
// 订单列表相关
|
|
|
|
|
|
const orderList = ref<OrderListItem[]>([]);
|
|
|
|
|
|
const currentPage = ref(1);
|
|
|
|
|
|
const pageSize = ref(10);
|
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 订单详情相关
|
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
|
const currentOrder = ref<OrderDetail | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取订单列表
|
|
|
|
|
|
const loadOrderList = async () => {
|
|
|
|
|
|
if (!userStore.userInfo) {
|
|
|
|
|
|
ElMessage.warning("请先登录");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getOrderList({
|
|
|
|
|
|
page: currentPage.value,
|
|
|
|
|
|
size: pageSize.value,
|
|
|
|
|
|
userId: userStore.userInfo.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
orderList.value = res.data.list;
|
|
|
|
|
|
total.value = res.data.total;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(res.message || "获取订单列表失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
ElMessage.error("获取订单列表失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理页面大小变化
|
|
|
|
|
|
const handleSizeChange = (val: number) => {
|
|
|
|
|
|
pageSize.value = val;
|
|
|
|
|
|
currentPage.value = 1;
|
|
|
|
|
|
loadOrderList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理页码变化
|
|
|
|
|
|
const handleCurrentChange = (val: number) => {
|
|
|
|
|
|
currentPage.value = val;
|
|
|
|
|
|
loadOrderList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 查看订单详情
|
|
|
|
|
|
const viewOrderDetail = async (order: OrderListItem) => {
|
|
|
|
|
|
if (!userStore.userInfo) {
|
|
|
|
|
|
ElMessage.warning("请先登录");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getOrderDetail(order.id, userStore.userInfo.id);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
currentOrder.value = res.data;
|
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(res.message || "获取订单详情失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
ElMessage.error("获取订单详情失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-21 02:21:26 +08:00
|
|
|
|
// 格式化日期 (用于列表中的日期)
|
|
|
|
|
|
const formatDate = (date: string | Date | number[]) => {
|
|
|
|
|
|
// 检查是否为空值
|
|
|
|
|
|
if (!date) return "-";
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let d: Date;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是数组格式 [year, month, day, hour, minute, second]
|
|
|
|
|
|
if (Array.isArray(date)) {
|
|
|
|
|
|
if (date.length < 6) return "-";
|
|
|
|
|
|
// 注意:JavaScript 中月份是从0开始的,所以需要减1
|
|
|
|
|
|
d = new Date(date[0], date[1] - 1, date[2], date[3], date[4], date[5]);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是字符串并且是 ISO 8601 格式 (如: 2025-12-21T00:04:18)
|
|
|
|
|
|
else if (typeof date === "string" && date.includes("T")) {
|
|
|
|
|
|
d = new Date(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 其他格式处理
|
|
|
|
|
|
else {
|
|
|
|
|
|
d = new Date(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为有效日期
|
|
|
|
|
|
if (isNaN(d.getTime())) return "-";
|
|
|
|
|
|
|
|
|
|
|
|
const year = d.getFullYear();
|
|
|
|
|
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
|
|
|
|
const hours = String(d.getHours()).padStart(2, "0");
|
|
|
|
|
|
const minutes = String(d.getMinutes()).padStart(2, "0");
|
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("日期格式化错误:", e, "输入值:", date);
|
|
|
|
|
|
return "-";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化日期时间 (用于详情中的日期时间)
|
|
|
|
|
|
const formatDateTime = (date: string | Date | number[]) => {
|
|
|
|
|
|
// 检查是否为空值
|
|
|
|
|
|
if (!date) return "-";
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let d: Date;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是数组格式 [year, month, day, hour, minute, second]
|
|
|
|
|
|
if (Array.isArray(date)) {
|
|
|
|
|
|
if (date.length < 6) return "-";
|
|
|
|
|
|
// 注意:JavaScript 中月份是从0开始的,所以需要减1
|
|
|
|
|
|
d = new Date(date[0], date[1] - 1, date[2], date[3], date[4], date[5]);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是字符串并且是 ISO 8601 格式 (如: 2025-12-21T00:04:18)
|
|
|
|
|
|
else if (typeof date === "string" && date.includes("T")) {
|
|
|
|
|
|
d = new Date(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 其他格式处理
|
|
|
|
|
|
else {
|
|
|
|
|
|
d = new Date(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为有效日期
|
|
|
|
|
|
if (isNaN(d.getTime())) return "-";
|
|
|
|
|
|
|
|
|
|
|
|
const year = d.getFullYear();
|
|
|
|
|
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
|
|
|
|
const hours = String(d.getHours()).padStart(2, "0");
|
|
|
|
|
|
const minutes = String(d.getMinutes()).padStart(2, "0");
|
|
|
|
|
|
const seconds = String(d.getSeconds()).padStart(2, "0");
|
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("日期时间格式化错误:", e, "输入值:", date);
|
|
|
|
|
|
return "-";
|
|
|
|
|
|
}
|
2025-12-20 20:41:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取订单状态文本
|
|
|
|
|
|
const getOrderStatusText = (status: number) => {
|
|
|
|
|
|
const statusMap: Record<number, string> = {
|
|
|
|
|
|
0: "待付款",
|
|
|
|
|
|
1: "待发货",
|
|
|
|
|
|
2: "待收货",
|
|
|
|
|
|
3: "已完成",
|
|
|
|
|
|
4: "已取消",
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || "未知状态";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取支付方式文本
|
|
|
|
|
|
const getPayTypeText = (payType: number) => {
|
|
|
|
|
|
const payTypeMap: Record<number, string> = {
|
|
|
|
|
|
0: "未支付",
|
|
|
|
|
|
1: "微信",
|
|
|
|
|
|
2: "支付宝",
|
|
|
|
|
|
};
|
|
|
|
|
|
return payTypeMap[payType] || "未知";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadOrderList();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.order-page {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.order-header {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-orders {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
margin: 20px 0 10px 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 商品信息样式仿照购物车 */
|
|
|
|
|
|
.product-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-image {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-details {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-name {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-sku {
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.order-items {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|