细节优化
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-03-30 14:05:26 +08:00
parent 2d0c71721d
commit 2dd04e11e4
6 changed files with 109 additions and 45 deletions

View File

@ -3,7 +3,7 @@
<div class="page-header">
<h2>评价管理</h2>
<div class="header-actions">
<el-input v-model="searchUid" placeholder="输入跑腿UID搜索" clearable style="width: 220px;"
<el-input v-model="searchUid" placeholder="输入UID搜索(单主或跑腿)" clearable style="width: 260px;"
@clear="onClearSearch" @keyup.enter="fetchList">
<template #prefix><el-icon><Search /></el-icon></template>
<template #append>
@ -35,8 +35,16 @@
<el-table :data="list" v-loading="loading" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="60" align="center" />
<el-table-column prop="orderNo" label="订单编号" min-width="180" show-overflow-tooltip />
<el-table-column prop="runnerId" label="跑腿UID" width="90" align="center" />
<el-table-column prop="runnerNickname" label="跑腿昵称" width="120" show-overflow-tooltip />
<el-table-column label="单主" width="150">
<template #default="{ row }">
{{ row.ownerNickname || '-' }} <span style="color: #999; font-size: 11px;">UID:{{ row.ownerId }}</span>
</template>
</el-table-column>
<el-table-column label="跑腿" width="150">
<template #default="{ row }">
{{ row.runnerNickname || '-' }} <span style="color: #999; font-size: 11px;">UID:{{ row.runnerId }}</span>
</template>
</el-table-column>
<el-table-column label="星级" width="120" align="center">
<template #default="{ row }">
<span style="color: #ff9900;">{{ '★'.repeat(row.rating) }}{{ '☆'.repeat(5 - row.rating) }}</span>
@ -89,10 +97,17 @@ async function fetchList() {
loading.value = true
userInfo.value = null
try {
// UID
const params = {}
if (searchUid.value) params.runnerId = searchUid.value
list.value = await request.get('/admin/reviews', { params })
// UID
let results = await request.get('/admin/reviews', { params })
// UIDUID
if (searchUid.value && results.length === 0) {
results = await request.get('/admin/reviews', { params: { ownerId: searchUid.value } })
}
list.value = results
if (searchUid.value) await loadUserInfo(searchUid.value)
} finally {
loading.value = false

View File

@ -70,17 +70,12 @@
<el-button size="small" type="success" plain>解封</el-button>
</template>
</el-popconfirm>
<el-popconfirm
<el-button
v-if="row.role !== 'Admin'"
title="删除后不可恢复,确定删除?"
confirm-button-text="删除"
confirm-button-type="danger"
@confirm="deleteUser(row)"
>
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
size="small"
type="danger"
@click="confirmDelete(row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
@ -89,7 +84,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import request from '../utils/request'
@ -120,6 +115,17 @@ async function deleteUser(row) {
fetchList()
}
async function confirmDelete(row) {
try {
await ElMessageBox.confirm(
`确定删除用户「${row.nickname}」(UID:${row.id})?删除后该用户的所有订单、评价、收益等数据将被清除,不可恢复。`,
'删除用户',
{ confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'el-button--danger' }
)
await deleteUser(row)
} catch {}
}
function getRoleLabel(role) {
const map = { User: '普通用户', Runner: '跑腿', Admin: '管理员' }
return map[role] || role

View File

@ -25,7 +25,7 @@
<image class="arrow-icon" src="/static/ic_arrow.png" mode="aspectFit"></image>
</view>
<view class="card-stats">
<view class="stat-item" @click.stop="goMyOrders('InProgress,WaitConfirm')">
<view class="stat-item" @click.stop="goMyOrders('InProgress')">
<text class="stat-label">进行中</text>
<text class="stat-num">{{ stats.orderOngoing }}</text>
</view>
@ -44,7 +44,7 @@
<image class="arrow-icon" src="/static/ic_arrow.png" mode="aspectFit"></image>
</view>
<view class="card-stats">
<view class="stat-item" @click.stop="goMyTaken('InProgress,WaitConfirm')">
<view class="stat-item" @click.stop="goMyTaken('InProgress')">
<text class="stat-label">进行中</text>
<text class="stat-num">{{ stats.takenOngoing }}</text>
</view>
@ -135,9 +135,9 @@ export default {
])
const orders = ordersRes?.items || ordersRes || []
const taken = takenRes?.items || takenRes || []
this.stats.orderOngoing = orders.filter(o => o.status !== 'Completed' && o.status !== 'Cancelled').length
this.stats.orderOngoing = orders.filter(o => o.status === 'InProgress' || o.status === 'WaitConfirm').length
this.stats.orderCompleted = orders.filter(o => o.status === 'Completed').length
this.stats.takenOngoing = taken.filter(o => o.status !== 'Completed' && o.status !== 'Cancelled').length
this.stats.takenOngoing = taken.filter(o => o.status === 'InProgress' || o.status === 'WaitConfirm').length
this.stats.takenCompleted = taken.filter(o => o.status === 'Completed').length
} catch (e) {
//

View File

@ -179,7 +179,8 @@ export default {
statusTabs: [
{ label: '全部', value: '' },
{ label: '待接单', value: 'Pending' },
{ label: '进行中', value: 'InProgress,WaitConfirm' },
{ label: '进行中', value: 'InProgress' },
{ label: '待确认', value: 'WaitConfirm' },
{ label: '已完成', value: 'Completed' },
{ label: '已取消', value: 'Cancelled' },
{ label: '申诉中', value: 'Appealing' }

View File

@ -123,7 +123,8 @@ export default {
return {
statusTabs: [
{ label: '全部', value: '' },
{ label: '进行中', value: 'InProgress,WaitConfirm' },
{ label: '进行中', value: 'InProgress' },
{ label: '待确认', value: 'WaitConfirm' },
{ label: '已完成', value: 'Completed' },
{ label: '已取消', value: 'Cancelled' }
],

View File

@ -134,24 +134,60 @@ public static class AdminEndpoints
if (user == null)
return Results.NotFound(new { code = 404, message = "用户不存在" });
// 不允许删除管理员
if (user.Role == UserRole.Admin)
return Results.BadRequest(new { code = 400, message = "不能删除管理员账号" });
// 删除关联数据
var certifications = db.RunnerCertifications.Where(c => c.UserId == id);
db.RunnerCertifications.RemoveRange(certifications);
try
{
// 获取该用户作为单主的订单ID列表
var ownedOrderIds = await db.Orders.Where(o => o.OwnerId == id).Select(o => o.Id).ToListAsync();
var reviews = db.Reviews.Where(r => r.RunnerId == id);
db.Reviews.RemoveRange(reviews);
if (ownedOrderIds.Count > 0)
{
// 先删除订单的子表数据
db.FoodOrderItems.RemoveRange(db.FoodOrderItems.Where(f => ownedOrderIds.Contains(f.OrderId)));
db.PriceChanges.RemoveRange(db.PriceChanges.Where(p => ownedOrderIds.Contains(p.OrderId)));
db.Appeals.RemoveRange(db.Appeals.Where(a => ownedOrderIds.Contains(a.OrderId)));
db.Reviews.RemoveRange(db.Reviews.Where(r => ownedOrderIds.Contains(r.OrderId)));
db.Earnings.RemoveRange(db.Earnings.Where(e => ownedOrderIds.Contains(e.OrderId)));
await db.SaveChangesAsync();
var earnings = db.Earnings.Where(e => e.UserId == id);
db.Earnings.RemoveRange(earnings);
// 再删除订单
db.Orders.RemoveRange(db.Orders.Where(o => ownedOrderIds.Contains(o.Id)));
await db.SaveChangesAsync();
}
db.Users.Remove(user);
await db.SaveChangesAsync();
// 将该用户作为跑腿的订单,清除跑腿关联
var takenOrders = await db.Orders.Where(o => o.RunnerId == id).ToListAsync();
foreach (var order in takenOrders)
{
order.RunnerId = null;
order.AcceptedAt = null;
if (order.Status == OrderStatus.InProgress || order.Status == OrderStatus.WaitConfirm)
order.Status = OrderStatus.Pending;
}
await db.SaveChangesAsync();
return Results.Ok(new { message = "用户已删除" });
// 删除用户自身的关联数据
db.RunnerCertifications.RemoveRange(db.RunnerCertifications.Where(c => c.UserId == id));
db.Reviews.RemoveRange(db.Reviews.Where(r => r.RunnerId == id));
db.Earnings.RemoveRange(db.Earnings.Where(e => e.UserId == id));
db.Withdrawals.RemoveRange(db.Withdrawals.Where(w => w.UserId == id));
db.MessageReads.RemoveRange(db.MessageReads.Where(m => m.UserId == id));
db.PriceChanges.RemoveRange(db.PriceChanges.Where(p => p.InitiatorId == id));
await db.SaveChangesAsync();
// 最后删除用户
db.Users.Remove(user);
await db.SaveChangesAsync();
return Results.Ok(new { message = "用户已删除" });
}
catch (Exception ex)
{
Console.WriteLine($"[管理端] 删除用户失败: {ex.InnerException?.Message ?? ex.Message}");
return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.InnerException?.Message ?? ex.Message}" });
}
}).RequireAuthorization("AdminOnly");
// 管理端获取跑腿列表
@ -250,30 +286,35 @@ public static class AdminEndpoints
}).RequireAuthorization("AdminOnly");
// 管理端获取评价列表
app.MapGet("/api/admin/reviews", async (int? runnerId, AppDbContext db) =>
app.MapGet("/api/admin/reviews", async (int? runnerId, int? ownerId, AppDbContext db) =>
{
var query = db.Reviews
.Include(r => r.Order)
.Include(r => r.Order).ThenInclude(o => o!.Owner)
.Include(r => r.Runner)
.AsQueryable();
if (runnerId.HasValue)
query = query.Where(r => r.RunnerId == runnerId.Value);
if (ownerId.HasValue)
query = query.Where(r => r.Order != null && r.Order.OwnerId == ownerId.Value);
var reviews = await query
.OrderByDescending(r => r.CreatedAt)
.Select(r => new AdminReviewResponse
.Select(r => new
{
Id = r.Id,
OrderId = r.OrderId,
r.Id,
r.OrderId,
OrderNo = r.Order != null ? r.Order.OrderNo : "",
RunnerId = r.RunnerId,
RunnerNickname = r.Runner != null ? r.Runner.Nickname : null,
Rating = r.Rating,
Content = r.Content,
ScoreChange = r.ScoreChange,
IsDisabled = r.IsDisabled,
CreatedAt = r.CreatedAt
OwnerId = r.Order != null ? r.Order.OwnerId : 0,
OwnerNickname = r.Order != null && r.Order.Owner != null ? r.Order.Owner.Nickname : "",
r.RunnerId,
RunnerNickname = r.Runner != null ? r.Runner.Nickname : "",
r.Rating,
r.Content,
r.ScoreChange,
r.IsDisabled,
r.CreatedAt
})
.ToListAsync();