campus-errand/admin/src/views/Runners.vue
18631081161 2d0c71721d
All checks were successful
continuous-integration/drone/push Build is passing
改bug
2026-03-29 21:13:50 +08:00

192 lines
6.5 KiB
Vue

<template>
<div class="runners-page">
<div class="page-header">
<h2>跑腿管理</h2>
<el-tag type="info" size="large"> {{ list.length }} 名跑腿</el-tag>
</div>
<el-table :data="list" v-loading="loading" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column label="评分" width="160" align="center">
<template #default="{ row }">
<div class="score-cell">
<el-progress
:percentage="row.runnerScore"
:color="getScoreColor(row.runnerScore)"
:stroke-width="10"
:show-text="false"
style="width: 80px; display: inline-block; vertical-align: middle;"
/>
<span class="score-text" :style="{ color: getScoreColor(row.runnerScore) }">{{ row.runnerScore }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="评价星级" width="120" align="center">
<template #default="{ row }">
<span v-if="row.reviewCount > 0" style="color: #ff9900; font-weight: bold;">
★ {{ row.avgRating }}
</span>
<span v-else style="color: #ccc;">暂无</span>
</template>
</el-table-column>
<el-table-column label="被评价" width="100" align="center">
<template #default="{ row }">
<el-link v-if="row.reviewCount > 0" type="primary" @click="showReviews(row)">
{{ row.reviewCount }}次
</el-link>
<span v-else style="color: #ccc;">0次</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isBanned ? 'danger' : 'success'" round size="small">
{{ row.isBanned ? '已封禁' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="注册时间" min-width="160">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-popconfirm
v-if="!row.isBanned"
title="确定封禁该跑腿?"
confirm-button-text="封禁"
confirm-button-type="danger"
@confirm="toggleBan(row, true)"
>
<template #reference>
<el-button size="small" type="danger" plain>封禁</el-button>
</template>
</el-popconfirm>
<el-popconfirm
v-else
title="确定解封该跑腿?"
confirm-button-text="解封"
@confirm="toggleBan(row, false)"
>
<template #reference>
<el-button size="small" type="success" plain>解封</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 评价记录弹窗 -->
<el-dialog v-model="reviewDialogVisible" :title="`${currentRunner?.nickname} 的评价记录`" width="700px">
<el-table :data="reviews" v-loading="reviewLoading" stripe size="small">
<el-table-column label="订单号" prop="orderNo" min-width="180" show-overflow-tooltip />
<el-table-column label="类型" width="80" align="center">
<template #default="{ row }">{{ getTypeLabel(row.orderType) }}</template>
</el-table-column>
<el-table-column label="星级" width="80" align="center">
<template #default="{ row }">
<span style="color: #ff9900;">{{ '★'.repeat(row.rating) }}</span>
</template>
</el-table-column>
<el-table-column label="分数变化" width="90" align="center">
<template #default="{ row }">
<span :style="{ color: row.scoreChange >= 0 ? '#67c23a' : '#f56c6c' }">
{{ row.scoreChange >= 0 ? '+' : '' }}{{ row.scoreChange }}
</span>
</template>
</el-table-column>
<el-table-column label="评价内容" prop="content" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.content || '-' }}</template>
</el-table-column>
<el-table-column label="时间" width="160">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
const loading = ref(false)
const list = ref([])
const reviewDialogVisible = ref(false)
const reviewLoading = ref(false)
const reviews = ref([])
const currentRunner = ref(null)
async function fetchList() {
loading.value = true
try {
list.value = await request.get('/admin/runners')
} finally {
loading.value = false
}
}
async function toggleBan(row, isBanned) {
const label = isBanned ? '封禁' : '解封'
await request.put(`/admin/runners/${row.id}/ban`, { isBanned })
ElMessage.success(`${label}`)
fetchList()
}
async function showReviews(row) {
currentRunner.value = row
reviewDialogVisible.value = true
reviewLoading.value = true
try {
reviews.value = await request.get(`/admin/runners/${row.id}/reviews`)
} finally {
reviewLoading.value = false
}
}
function getScoreColor(score) {
if (score >= 80) return '#67c23a'
if (score >= 60) return '#e6a23c'
return '#f56c6c'
}
function getTypeLabel(type) {
const map = { Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }
return map[type] || type
}
function formatTime(str) {
if (!str) return '-'
const d = new Date(str)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
onMounted(fetchList)
</script>
<style scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
font-size: 20px;
}
.score-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.score-text {
font-weight: bold;
font-size: 14px;
min-width: 24px;
}
</style>