feat: 用户管理显示资格和消费金额,订单管理添加详情功能
- 用户管理: 显示资格数和消费金额,支持编辑资格 - 订单管理: 添加订单详情查看功能 - 后端: AdminGetUsers返回消费金额, AdminGetByID获取订单详情 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
@@ -392,6 +427,7 @@ func (h *AuthHandler) AdminUpdateUser(c *gin.Context) {
|
||||
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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="viewDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.refund_status === 'pending'" link type="primary" size="small" @click="processRefund(row.id, 'approved')">批准</el-button>
|
||||
<el-button v-if="row.refund_status === 'pending'" link type="danger" size="small" @click="processRefund(row.id, 'rejected')">拒绝</el-button>
|
||||
</template>
|
||||
@@ -30,6 +31,118 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="订单详情" width="700px" top="5vh">
|
||||
<div v-if="currentOrder" class="order-detail">
|
||||
<div class="detail-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">订单号:</span>
|
||||
<span class="value">#{{ currentOrder.id }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">用户:</span>
|
||||
<span class="value">{{ currentOrder.user?.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value"><el-tag>{{ statusText(currentOrder.status) }}</el-tag></span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">创建时间:</span>
|
||||
<span class="value">{{ formatDate(currentOrder.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentOrder.shipping_address">
|
||||
<h4>收货地址</h4>
|
||||
<div class="address-info">
|
||||
<p><strong>{{ currentOrder.shipping_address.name }}</strong> {{ currentOrder.shipping_address.phone }}</p>
|
||||
<p>{{ currentOrder.shipping_address.province }}{{ currentOrder.shipping_address.city }}{{ currentOrder.shipping_address.district }}{{ currentOrder.shipping_address.address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>商品清单</h4>
|
||||
<el-table :data="currentOrder.order_items" size="small">
|
||||
<el-table-column label="商品" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="product-info">
|
||||
<span class="product-name">{{ row.product?.name || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="80" align="center" />
|
||||
<el-table-column label="单价" width="100" align="right">
|
||||
<template #default="{ row }">¥{{ row.price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="100" align="right">
|
||||
<template #default="{ row }">¥{{ (row.price * row.quantity).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>费用明细</h4>
|
||||
<div class="fee-list">
|
||||
<div class="fee-item">
|
||||
<span>商品金额</span>
|
||||
<span>¥{{ (currentOrder.subtotal || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="fee-item">
|
||||
<span>运费</span>
|
||||
<span>¥{{ (currentOrder.shipping_fee || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="fee-item" v-if="currentOrder.service_fee > 0">
|
||||
<span>服务费</span>
|
||||
<span>¥{{ (currentOrder.service_fee || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="fee-item" v-if="currentOrder.tax > 0">
|
||||
<span>税费</span>
|
||||
<span>¥{{ (currentOrder.tax || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="fee-item total">
|
||||
<span>合计</span>
|
||||
<span class="total-amount">¥{{ (currentOrder.total_amount || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentOrder.tracking_number">
|
||||
<h4>物流信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">快递单号:</span>
|
||||
<span class="value">{{ currentOrder.tracking_number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentOrder.refund_status">
|
||||
<h4>退款信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">退款状态:</span>
|
||||
<span class="value"><el-tag type="warning">{{ currentOrder.refund_status }}</el-tag></span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentOrder.refund_reason">
|
||||
<span class="label">退款原因:</span>
|
||||
<span class="value">{{ currentOrder.refund_reason }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="currentOrder.refund_amount">
|
||||
<span class="label">退款金额:</span>
|
||||
<span class="value">¥{{ currentOrder.refund_amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -47,6 +160,9 @@ const paginatedOrders = computed(() => {
|
||||
return orders.value.slice(start, start + pageSize)
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const currentOrder = ref<any>(null)
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="table-card">
|
||||
<el-table :data="paginatedUsers">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="username" label="用户名" min-width="100" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="150" />
|
||||
<el-table-column prop="role" label="角色" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : row.role === 'supplier' ? 'warning' : 'info'">
|
||||
@@ -12,7 +12,16 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="purchase_credits" label="积分" width="100" />
|
||||
<el-table-column prop="purchase_credits" label="资格" width="80">
|
||||
<template #default="{ row }">
|
||||
<span class="credit-value">{{ row.purchase_credits }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_spent" label="消费金额" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="spent-value">¥{{ (row.total_spent || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_active" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">
|
||||
@@ -20,7 +29,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="注册时间" width="180">
|
||||
<el-table-column prop="created_at" label="注册时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
@@ -58,6 +67,9 @@
|
||||
<el-option label="管理员" value="admin" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="资格">
|
||||
<el-input-number v-model="editForm.purchase_credits" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="editForm.is_active" active-text="启用" inactive-text="禁用" />
|
||||
</el-form-item>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user