This commit is contained in:
parent
2d0c71721d
commit
2dd04e11e4
|
|
@ -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 })
|
||||
|
||||
// 如果跑腿UID没搜到结果,再按单主UID搜索
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// 静默处理
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user