diff --git a/backend/internal/api/handlers/auth.go b/backend/internal/api/handlers/auth.go index d59497e..0454723 100644 --- a/backend/internal/api/handlers/auth.go +++ b/backend/internal/api/handlers/auth.go @@ -376,7 +376,42 @@ func (h *AuthHandler) Install(c *gin.Context) { func (h *AuthHandler) AdminGetUsers(c *gin.Context) { var users []models.User utils.DB.Select("id, username, email, role, purchase_credits, is_active, email_verified, created_at").Find(&users) - c.JSON(http.StatusOK, gin.H{"data": users}) + + // 计算每个用户的消费金额 + type UserWithSpent struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` + PurchaseCredits int `json:"purchase_credits"` + IsActive bool `json:"is_active"` + EmailVerified bool `json:"email_verified"` + CreatedAt time.Time `json:"created_at"` + TotalSpent float64 `json:"total_spent"` + } + + result := make([]UserWithSpent, len(users)) + for i, user := range users { + var totalSpent float64 + utils.DB.Model(&models.Order{}). + Where("user_id = ? AND status IN ?", user.ID, []string{"completed", "shipped"}). + Select("COALESCE(SUM(total_amount), 0)"). + Scan(&totalSpent) + + result[i] = UserWithSpent{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + PurchaseCredits: user.PurchaseCredits, + IsActive: user.IsActive, + EmailVerified: user.EmailVerified, + CreatedAt: user.CreatedAt, + TotalSpent: totalSpent, + } + } + + c.JSON(http.StatusOK, gin.H{"data": result}) } func (h *AuthHandler) AdminUpdateUser(c *gin.Context) { @@ -388,10 +423,11 @@ func (h *AuthHandler) AdminUpdateUser(c *gin.Context) { } var req struct { - Username *string `json:"username"` - Email *string `json:"email"` - Role *string `json:"role"` - IsActive *bool `json:"is_active"` + Username *string `json:"username"` + Email *string `json:"email"` + Role *string `json:"role"` + IsActive *bool `json:"is_active"` + PurchaseCredits *int `json:"purchase_credits"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -411,6 +447,9 @@ func (h *AuthHandler) AdminUpdateUser(c *gin.Context) { if req.IsActive != nil { updates["is_active"] = *req.IsActive } + if req.PurchaseCredits != nil { + updates["purchase_credits"] = *req.PurchaseCredits + } utils.DB.Model(&user).Updates(updates) utils.DB.First(&user, id) diff --git a/backend/internal/api/handlers/order.go b/backend/internal/api/handlers/order.go index 4f7ebc7..a14cc19 100644 --- a/backend/internal/api/handlers/order.go +++ b/backend/internal/api/handlers/order.go @@ -503,6 +503,20 @@ func (h *OrderHandler) AdminList(c *gin.Context) { }) } +func (h *OrderHandler) AdminGetByID(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + + var order models.Order + if err := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").First(&order, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + + checkOrderExpired(&order) + + c.JSON(http.StatusOK, gin.H{"data": order}) +} + func (h *OrderHandler) ProcessRefund(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index c283338..9794b71 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -174,6 +174,7 @@ func SetupRoutes(r *gin.Engine) { adminOrders := admin.Group("/orders") { adminOrders.GET("", orderHandler.AdminList) + adminOrders.GET("/:id", orderHandler.AdminGetByID) adminOrders.GET("/export", orderHandler.Export) adminOrders.PUT("/:id/refund", orderHandler.ProcessRefund) } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 44cd3c4..9eb661e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -136,6 +136,7 @@ export const adminApi = { addCustomField: (id: number, data: any) => api.post(`/admin/products/${id}/custom-fields`, data), getOrders: (params?: any) => api.get('/admin/orders', { params }), + getOrderById: (id: number) => api.get(`/admin/orders/${id}`), exportOrders: () => api.get('/admin/orders/export'), processRefund: (id: number, data: any) => api.put(`/admin/orders/${id}/refund`, data), diff --git a/frontend/src/views/admin/Orders.vue b/frontend/src/views/admin/Orders.vue index d7fbc6b..b4458d1 100644 --- a/frontend/src/views/admin/Orders.vue +++ b/frontend/src/views/admin/Orders.vue @@ -13,8 +13,9 @@ - + @@ -30,6 +31,118 @@ /> + + + +
+
+

基本信息

+
+
+ 订单号: + #{{ currentOrder.id }} +
+
+ 用户: + {{ currentOrder.user?.username || '-' }} +
+
+ 状态: + {{ statusText(currentOrder.status) }} +
+
+ 创建时间: + {{ formatDate(currentOrder.created_at) }} +
+
+
+ +
+

收货地址

+
+

{{ currentOrder.shipping_address.name }} {{ currentOrder.shipping_address.phone }}

+

{{ currentOrder.shipping_address.province }}{{ currentOrder.shipping_address.city }}{{ currentOrder.shipping_address.district }}{{ currentOrder.shipping_address.address }}

+
+
+ +
+

商品清单

+ + + + + + + + + + + + +
+ +
+

费用明细

+
+
+ 商品金额 + ¥{{ (currentOrder.subtotal || 0).toFixed(2) }} +
+
+ 运费 + ¥{{ (currentOrder.shipping_fee || 0).toFixed(2) }} +
+
+ 服务费 + ¥{{ (currentOrder.service_fee || 0).toFixed(2) }} +
+
+ 税费 + ¥{{ (currentOrder.tax || 0).toFixed(2) }} +
+
+ 合计 + ¥{{ (currentOrder.total_amount || 0).toFixed(2) }} +
+
+
+ +
+

物流信息

+
+
+ 快递单号: + {{ currentOrder.tracking_number }} +
+
+
+ +
+

退款信息

+
+
+ 退款状态: + {{ currentOrder.refund_status }} +
+
+ 退款原因: + {{ currentOrder.refund_reason }} +
+
+ 退款金额: + ¥{{ currentOrder.refund_amount.toFixed(2) }} +
+
+
+
+ +
@@ -47,6 +160,9 @@ const paginatedOrders = computed(() => { return orders.value.slice(start, start + pageSize) }) +const detailVisible = ref(false) +const currentOrder = ref(null) + const statusMap: Record = { pending_payment: '待支付', pending_confirm: '待确认', pending_ship: '待发货', shipped: '已发货', completed: '已完成', refunding: '退款中', refunded: '已退款', cancelled: '已取消', @@ -73,6 +189,16 @@ async function fetchOrders() { orders.value = res.data || [] } +async function viewDetail(row: any) { + try { + const res: any = await adminApi.getOrderById(row.id) + currentOrder.value = res.data + detailVisible.value = true + } catch { + ElMessage.error('获取订单详情失败') + } +} + async function processRefund(id: number, status: string) { await adminApi.processRefund(id, { status }) ElMessage.success('已处理') @@ -164,4 +290,93 @@ onMounted(fetchOrders) .pagination-wrap :deep(.el-pagination__total) { color: rgba(255, 255, 255, 0.5); } + +// 订单详情样式 +.order-detail { + .detail-section { + margin-bottom: 24px; + + h4 { + color: #fff; + font-size: 15px; + font-weight: 600; + margin: 0 0 12px 0; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } + } + + .detail-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + .detail-item { + .label { + color: rgba(255, 255, 255, 0.5); + font-size: 13px; + } + .value { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + } + } + + .address-info { + background: rgba(255, 255, 255, 0.03); + padding: 12px 16px; + border-radius: 8px; + + p { + color: rgba(255, 255, 255, 0.8); + margin: 0; + line-height: 1.6; + + &:first-child { + margin-bottom: 4px; + } + + strong { + color: #fff; + } + } + } + + .product-info { + .product-name { + color: rgba(255, 255, 255, 0.9); + } + } + + .fee-list { + .fee-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + + &.total { + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin-top: 8px; + padding-top: 12px; + color: #fff; + font-weight: 500; + + .total-amount { + color: #f56c6c; + font-size: 18px; + font-weight: 700; + } + } + } + } +} + +@media (max-width: 768px) { + .detail-grid { + grid-template-columns: 1fr !important; + } +} diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue index 5383d9e..aeb6d79 100644 --- a/frontend/src/views/admin/Users.vue +++ b/frontend/src/views/admin/Users.vue @@ -3,8 +3,8 @@
- - + + - + + + + + + - + @@ -58,6 +67,9 @@ + + + @@ -126,6 +138,7 @@ async function handleEdit() { username: editForm.value.username, email: editForm.value.email, role: editForm.value.role, + purchase_credits: editForm.value.purchase_credits, is_active: editForm.value.is_active, }) ElMessage.success('更新成功') @@ -152,6 +165,16 @@ async function handleDelete(row: any) { .users-page { padding: 0; } .table-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 20px; } +.credit-value { + color: #f59e0b; + font-weight: 600; +} + +.spent-value { + color: #10b981; + font-weight: 600; +} + .pagination-wrap { margin-top: 20px; display: flex;