All checks were successful
continuous-integration/drone/push Build is passing
192 lines
6.5 KiB
Vue
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>
|