All checks were successful
continuous-integration/drone/push Build is passing
159 lines
4.9 KiB
Vue
159 lines
4.9 KiB
Vue
<template>
|
|
<div class="users-page">
|
|
<div class="page-header">
|
|
<h2>用户管理</h2>
|
|
<div class="header-actions">
|
|
<el-input
|
|
v-model="keyword"
|
|
placeholder="搜索昵称/手机号/ID"
|
|
clearable
|
|
style="width: 240px"
|
|
@keyup.enter="fetchList"
|
|
@clear="fetchList"
|
|
>
|
|
<template #append>
|
|
<el-button @click="fetchList" :icon="Search" />
|
|
</template>
|
|
</el-input>
|
|
<el-tag type="info" size="large" style="margin-left: 12px">共 {{ list.length }} 名用户</el-tag>
|
|
</div>
|
|
</div>
|
|
|
|
<el-table :data="list" v-loading="loading" stripe style="width: 100%">
|
|
<el-table-column prop="id" label="UID" width="80" align="center" />
|
|
<el-table-column label="头像" width="70" align="center">
|
|
<template #default="{ row }">
|
|
<el-avatar :size="36" :src="row.avatarUrl || undefined">
|
|
<template #default>
|
|
<span style="font-size: 14px">{{ (row.nickname || '?')[0] }}</span>
|
|
</template>
|
|
</el-avatar>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
|
|
<el-table-column label="角色" width="100" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getRoleType(row.role)" size="small" round>{{ getRoleLabel(row.role) }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="下单数" width="90" align="center" prop="orderCount" />
|
|
<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 label="注册时间" min-width="170">
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="160" 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>
|
|
<el-popconfirm
|
|
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>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import request from '../utils/request'
|
|
|
|
const loading = ref(false)
|
|
const list = ref([])
|
|
const keyword = ref('')
|
|
|
|
async function fetchList() {
|
|
loading.value = true
|
|
try {
|
|
const params = keyword.value ? { keyword: keyword.value } : {}
|
|
list.value = await request.get('/admin/users', { params })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleBan(row, isBanned) {
|
|
const label = isBanned ? '封禁' : '解封'
|
|
await request.put(`/admin/users/${row.id}/ban`, { isBanned })
|
|
ElMessage.success(`已${label}`)
|
|
fetchList()
|
|
}
|
|
|
|
async function deleteUser(row) {
|
|
await request.delete(`/admin/users/${row.id}`)
|
|
ElMessage.success('用户已删除')
|
|
fetchList()
|
|
}
|
|
|
|
function getRoleLabel(role) {
|
|
const map = { User: '普通用户', Runner: '跑腿', Admin: '管理员' }
|
|
return map[role] || role
|
|
}
|
|
|
|
function getRoleType(role) {
|
|
const map = { Admin: 'warning', Runner: 'success' }
|
|
return map[role] || ''
|
|
}
|
|
|
|
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;
|
|
}
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
</style>
|