修复订单系统多项问题:Token认证、购物车、订单创建、发货功能等

This commit is contained in:
王子琦
2026-02-10 12:24:34 +08:00
parent f48acbe97b
commit 02b48c0250
26 changed files with 1801 additions and 59 deletions

1407
meiruo-frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -84,6 +84,7 @@ export const orderApi = {
adminList: (params) => request.get('/order/admin/list', { params }),
getById: (id) => request.get(`/order/${id}`),
updateStatus: (id, status) => request.put(`/order/status/${id}`, null, { params: { status } }),
shipOrder: (id, data) => request.post(`/order/ship/${id}`, data),
getRevenue: (type) => request.get(`/order/revenue/${type}`),
getTopProducts: (limit) => request.get(`/order/top/${limit}`)
}

View File

@@ -1,6 +1,6 @@
<template>
<el-footer class="footer">
<p>© 2024 美若彩妆销售平台 版权所有</p>
<p>© 2026 美若彩妆销售平台 版权所有</p>
</el-footer>
</template>

View File

@@ -12,7 +12,12 @@ export const useUserStore = defineStore('user', () => {
async function login(username, password) {
const res = await userApi.login({ username, password })
if (res.code === 200) {
token.value = res.data.token || 'token'
const tokenValue = res.data?.token
if (!tokenValue || tokenValue === 'token') {
console.error('登录失败:后端未返回有效的 token', res)
return false
}
token.value = tokenValue
userInfo.value = res.data
localStorage.setItem('token', token.value)
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))

View File

@@ -17,7 +17,7 @@
<el-table-column label="轮播图" width="220">
<template #default="{ row }">
<div class="banner-cell">
<img :src="row.image || '/images/default.png'" :alt="row.title" class="banner-thumb" />
<img :src="getImageUrl(row.image)" :alt="row.title" class="banner-thumb" />
</div>
</template>
</el-table-column>
@@ -140,6 +140,12 @@ const rules = {
image: [{ required: true, message: '请上传轮播图', trigger: 'blur' }]
}
const getImageUrl = (url) => {
if (!url) return '/images/default.png'
if (url.startsWith('http') || url.startsWith('/upload')) return url
return '/upload' + url
}
const fetchBanners = async () => {
try {
const res = await bannerApi.getList()

View File

@@ -20,12 +20,12 @@
</template>
<el-table :data="orderList" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="order_no" label="订单号" width="200" />
<el-table-column prop="receiver_name" label="收货人" width="120" />
<el-table-column prop="receiver_phone" label="联系电话" width="140" />
<el-table-column prop="total_amount" label="订单金额">
<el-table-column prop="orderNo" label="订单号" width="200" />
<el-table-column prop="receiverName" label="收货人" width="120" />
<el-table-column prop="receiverPhone" label="联系电话" width="140" />
<el-table-column prop="totalAmount" label="订单金额">
<template #default="{ row }">
¥{{ parseFloat(row.total_amount).toFixed(2) }}
¥{{ row.totalAmount ? parseFloat(row.totalAmount).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
@@ -35,15 +35,25 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="下单时间" width="180" />
<el-table-column prop="logisticsCompany" label="物流公司" width="120">
<template #default="{ row }">
{{ row.logisticsCompany || '-' }}
</template>
</el-table-column>
<el-table-column prop="trackingNo" label="物流单号" width="150">
<template #default="{ row }">
{{ row.trackingNo || '-' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="下单时间" width="180" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button type="primary" size="small" @click="viewDetail(row)">查看</el-button>
<el-button
v-if="row.status === 1"
v-if="row.status === 2"
type="success"
size="small"
@click="updateStatus(row.id, 2)"
@click="openShipDialog(row)"
>
发货
</el-button>
@@ -51,16 +61,61 @@
</el-table-column>
</el-table>
</el-card>
<!-- 发货弹窗 -->
<el-dialog v-model="shipDialogVisible" title="订单发货" width="500px">
<el-form :model="shipForm" label-width="100px" :rules="shipRules" ref="shipFormRef">
<el-form-item label="订单号">
<span>{{ currentOrder?.orderNo }}</span>
</el-form-item>
<el-form-item label="收货人">
<span>{{ currentOrder?.receiverName }}</span>
</el-form-item>
<el-form-item label="物流公司" prop="logisticsCompany">
<el-select v-model="shipForm.logisticsCompany" placeholder="请选择物流公司" style="width: 100%">
<el-option label="顺丰速运" value="顺丰速运" />
<el-option label="中通快递" value="中通快递" />
<el-option label="圆通速递" value="圆通速递" />
<el-option label="韵达快递" value="韵达快递" />
<el-option label="申通快递" value="申通快递" />
<el-option label="EMS" value="EMS" />
<el-option label="京东物流" value="京东物流" />
<el-option label="德邦快递" value="德邦快递" />
</el-select>
</el-form-item>
<el-form-item label="物流单号" prop="trackingNo">
<el-input v-model="shipForm.trackingNo" placeholder="请输入物流单号" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="shipDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitShip" :loading="shipLoading">确认发货</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, reactive } from 'vue'
import { orderApi } from '../../api'
import { ElMessage } from 'element-plus'
const orderList = ref([])
const keyword = ref('')
const shipDialogVisible = ref(false)
const shipLoading = ref(false)
const currentOrder = ref(null)
const shipFormRef = ref(null)
const shipForm = reactive({
logisticsCompany: '',
trackingNo: ''
})
const shipRules = {
logisticsCompany: [{ required: true, message: '请选择物流公司', trigger: 'change' }],
trackingNo: [{ required: true, message: '请输入物流单号', trigger: 'blur' }]
}
const statusMap = {
1: '待付款',
@@ -91,14 +146,39 @@ const viewDetail = (order) => {
console.log('查看订单详情', order)
}
const updateStatus = async (id, status) => {
try {
await orderApi.updateStatus(id, status)
ElMessage.success('操作成功')
fetchOrders()
} catch (e) {
console.error(e)
}
const openShipDialog = (order) => {
currentOrder.value = order
shipForm.logisticsCompany = ''
shipForm.trackingNo = ''
shipDialogVisible.value = true
}
const submitShip = async () => {
if (!shipFormRef.value) return
await shipFormRef.value.validate(async (valid) => {
if (valid) {
shipLoading.value = true
try {
const res = await orderApi.shipOrder(currentOrder.value.id, {
logisticsCompany: shipForm.logisticsCompany,
trackingNo: shipForm.trackingNo
})
if (res.code === 200) {
ElMessage.success('发货成功')
shipDialogVisible.value = false
fetchOrders()
} else {
ElMessage.error(res.msg || '发货失败')
}
} catch (e) {
console.error(e)
ElMessage.error('发货失败')
} finally {
shipLoading.value = false
}
}
})
}
onMounted(() => {

View File

@@ -17,7 +17,7 @@
<el-table-column label="商品信息" min-width="280">
<template #default="{ row }">
<div class="product-cell">
<img :src="row.image || '/images/default.png'" :alt="row.name" class="product-thumb" />
<img :src="getImageUrl(row.image)" :alt="row.name" class="product-thumb" />
<div class="product-info">
<span class="product-name">{{ row.name }}</span>
<span class="product-desc">{{ row.description }}</span>
@@ -184,6 +184,12 @@ const getCategoryName = (categoryId) => {
return cat ? cat.name : '-'
}
const getImageUrl = (url) => {
if (!url) return '/images/default.png'
if (url.startsWith('http') || url.startsWith('/upload')) return url
return '/upload' + url
}
const fetchProducts = async () => {
try {
const res = await productApi.getList()

View File

@@ -11,13 +11,13 @@
<el-table :data="cartList" stripe style="width: 100%">
<el-table-column width="180">
<template #default="{ row }">
<el-image :src="row.product_image || '/images/default.png'" fit="cover" style="width: 100px; height: 100px" />
<el-image :src="row.productImage || '/images/default.png'" fit="cover" style="width: 100px; height: 100px" />
</template>
</el-table-column>
<el-table-column prop="product_name" label="商品名称" />
<el-table-column prop="productName" label="商品名称" />
<el-table-column label="单价">
<template #default="{ row }">
¥{{ parseFloat(row.product_price).toFixed(2) }}
¥{{ row.productPrice ? parseFloat(row.productPrice).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="数量" width="180">
@@ -27,7 +27,7 @@
</el-table-column>
<el-table-column label="小计">
<template #default="{ row }">
¥{{ (parseFloat(row.product_price) * row.quantity).toFixed(2) }}
¥{{ row.productPrice ? (parseFloat(row.productPrice) * row.quantity).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="操作" width="120">
@@ -66,7 +66,7 @@ const cartList = ref([])
const totalPrice = computed(() => {
return cartList.value.reduce((sum, item) => {
return sum + parseFloat(item.product_price) * item.quantity
return sum + (item.productPrice ? parseFloat(item.productPrice) : 0) * item.quantity
}, 0)
})

View File

@@ -29,12 +29,12 @@
<h3>订单商品</h3>
<div class="product-list">
<div class="product-item" v-for="item in orderItems" :key="item.id">
<img :src="item.product_image || '/images/default.png'" alt="" />
<img :src="item.productImage || '/images/default.png'" alt="" />
<div class="info">
<h4>{{ item.product_name }}</h4>
<p>¥{{ parseFloat(item.product_price).toFixed(2) }} x {{ item.quantity }}</p>
<h4>{{ item.productName }}</h4>
<p>¥{{ item.productPrice ? parseFloat(item.productPrice).toFixed(2) : '0.00' }} x {{ item.quantity }}</p>
</div>
<div class="subtotal">¥{{ (parseFloat(item.product_price) * item.quantity).toFixed(2) }}</div>
<div class="subtotal">¥{{ item.productPrice ? (parseFloat(item.productPrice) * item.quantity).toFixed(2) : '0.00' }}</div>
</div>
</div>
<div class="order-total">
@@ -82,7 +82,7 @@ const rules = {
const totalAmount = computed(() => {
return orderItems.value.reduce((sum, item) => {
return sum + parseFloat(item.product_price) * item.quantity
return sum + (item.productPrice ? parseFloat(item.productPrice) : 0) * item.quantity
}, 0)
})
@@ -96,10 +96,10 @@ const fetchData = async () => {
if (res.code === 200) {
orderItems.value = [{
id: 0,
product_id: res.data.id,
product_name: res.data.name,
product_image: res.data.image,
product_price: res.data.price,
productId: res.data.id,
productName: res.data.name,
productImage: res.data.image,
productPrice: res.data.price,
quantity: parseInt(router.currentRoute.value.query.quantity) || 1
}]
}
@@ -122,8 +122,14 @@ const handleSubmit = async () => {
submitting.value = true
const cartIds = orderItems.value.filter(item => item.id > 0).map(item => item.id)
const directItems = orderItems.value.filter(item => item.id === 0).map(item => ({
productId: item.productId,
quantity: item.quantity
}))
const res = await orderApi.create({
cartIds,
cartIds: cartIds.length > 0 ? cartIds : undefined,
items: directItems.length > 0 ? directItems : undefined,
...form.value
})

View File

@@ -9,7 +9,7 @@
<el-carousel :interval="5000" height="450px" indicator-position="outside" trigger="click">
<el-carousel-item v-for="banner in banners" :key="banner.id">
<div class="banner-item" @click="handleBannerClick(banner)">
<img :src="banner.image || '/images/default.png'" :alt="banner.title" class="banner-img" />
<img :src="getImageUrl(banner.image)" :alt="banner.title" class="banner-img" />
<div class="banner-overlay">
<h3>{{ banner.title }}</h3>
</div>
@@ -63,7 +63,7 @@
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="product in productList" :key="product.id">
<div class="product-card" @click="goToDetail(product.id)">
<div class="product-image-wrapper">
<img :src="product.image || '/images/default.png'" :alt="product.name" class="product-img" />
<img :src="getImageUrl(product.image)" :alt="product.name" class="product-img" />
<div class="product-badge" v-if="product.sales > 50">热销</div>
</div>
<div class="product-info">
@@ -108,6 +108,12 @@ const categories = ref([])
const productList = ref([])
const activeCategory = ref(null)
const getImageUrl = (url) => {
if (!url) return '/images/default.png'
if (url.startsWith('http') || url.startsWith('/upload')) return url
return '/upload' + url
}
const fetchBanners = async () => {
try {
const res = await bannerApi.getList(1)

View File

@@ -10,26 +10,33 @@
<el-tabs v-model="activeTab" @tab-change="fetchOrders">
<el-tab-pane label="全部" name="all" />
<el-tab-pane label="待付款" name="1" />
<el-tab-pane label="已付款" name="2" />
<el-tab-pane label="待发货" name="2" />
<el-tab-pane label="已发货" name="3" />
<el-tab-pane label="已完成" name="4" />
</el-tabs>
<div class="order-list" v-if="orderList.length > 0">
<div class="order-item" v-for="order in orderList" :key="order.id">
<div class="order-header">
<span class="order-no">订单号: {{ order.order_no }}</span>
<span class="order-no">订单号: {{ order.orderNo }}</span>
<span class="order-status">{{ getStatusText(order.status) }}</span>
</div>
<div class="order-body">
<div class="order-info">
<p>收货人: {{ order.receiver_name }}</p>
<p>联系电话: {{ order.receiver_phone }}</p>
<p>收货地址: {{ order.receiver_address }}</p>
<p>下单时间: {{ order.create_time }}</p>
<p>收货人: {{ order.receiverName }}</p>
<p>联系电话: {{ order.receiverPhone }}</p>
<p>收货地址: {{ order.receiverAddress }}</p>
<p>下单时间: {{ order.createTime }}</p>
<p v-if="order.logisticsCompany" class="logistics-info">
<el-icon><Van /></el-icon>
物流: {{ order.logisticsCompany }} - {{ order.trackingNo }}
</p>
<p v-if="order.shipTime" class="ship-time">
发货时间: {{ order.shipTime }}
</p>
</div>
<div class="order-total">
<span>订单总额:</span>
<span class="amount">¥{{ parseFloat(order.total_amount).toFixed(2) }}</span>
<span class="amount">¥{{ order.totalAmount ? parseFloat(order.totalAmount).toFixed(2) : '0.00' }}</span>
</div>
</div>
<div class="order-footer">
@@ -42,6 +49,14 @@
>
去付款
</el-button>
<el-button
v-if="order.status === 3"
type="success"
size="small"
@click="confirmReceive(order)"
>
确认收货
</el-button>
</div>
</div>
</div>
@@ -57,17 +72,18 @@
<script setup>
import { ref, onMounted } from 'vue'
import { Van } from '@element-plus/icons-vue'
import Header from '../../components/Header.vue'
import Footer from '../../components/Footer.vue'
import { orderApi } from '../../api'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
const activeTab = ref('all')
const orderList = ref([])
const statusMap = {
1: '待付款',
2: '已付款',
2: '待发货',
3: '已发货',
4: '已完成',
5: '已取消'
@@ -103,6 +119,23 @@ const payOrder = async (order) => {
}
}
const confirmReceive = async (order) => {
try {
await ElMessageBox.confirm('确认已收到商品?', '确认收货', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
await orderApi.updateStatus(order.id, 4)
ElMessage.success('确认收货成功')
fetchOrders()
} catch (e) {
if (e !== 'cancel') {
console.error(e)
}
}
}
onMounted(() => {
fetchOrders()
})
@@ -157,6 +190,20 @@ onMounted(() => {
margin-bottom: 5px;
color: #666;
}
.logistics-info {
color: #409EFF;
font-weight: 500;
.el-icon {
margin-right: 5px;
}
}
.ship-time {
color: #67C23A;
font-size: 13px;
}
}
.order-total {

View File

@@ -7,7 +7,7 @@
<el-main>
<div class="detail-container" v-if="product">
<div class="product-gallery">
<el-image :src="product.image || '/images/default.png'" :preview-src-list="[product.image]" fit="cover" />
<el-image :src="getImageUrl(product.image)" :preview-src-list="[getImageUrl(product.image)]" fit="cover" />
</div>
<div class="product-info">
<h1>{{ product.name }}</h1>
@@ -50,6 +50,12 @@ const userStore = useUserStore()
const product = ref(null)
const quantity = ref(1)
const getImageUrl = (url) => {
if (!url) return '/images/default.png'
if (url.startsWith('http') || url.startsWith('/upload')) return url
return '/upload' + url
}
const fetchProduct = async () => {
try {
const res = await productApi.getById(route.params.id)

View File

@@ -12,6 +12,10 @@ export default defineConfig({
server: {
port: 3000,
proxy: {
'/upload': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/api': {
target: 'http://localhost:8080',
changeOrigin: true