Files
zsglpt/admin-frontend/src/pages/UsersPage.vue

322 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { inject, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
adminResetUserPassword,
approveUser,
deleteUser,
fetchAllUsers,
rejectUser,
removeUserVip,
setUserVip,
} from '../api/users'
import { parseSqliteDateTime } from '../utils/datetime'
import { validatePasswordStrength } from '../utils/password'
const refreshStats = inject('refreshStats', null)
const loading = ref(false)
const users = ref([])
function isVip(user) {
const expire = user?.vip_expire_time
if (!expire) return false
if (String(expire).startsWith('2099-12-31')) return true
const dt = parseSqliteDateTime(expire)
return dt ? dt.getTime() > Date.now() : false
}
function vipLabel(user) {
const expire = user?.vip_expire_time
if (!expire || !isVip(user)) return ''
if (String(expire).startsWith('2099-12-31')) return '永久VIP'
const dt = parseSqliteDateTime(expire)
if (!dt) return `到期: ${expire}`
const daysLeft = Math.ceil((dt.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
return `到期: ${expire}(剩${daysLeft}天)`
}
function statusMeta(status) {
if (status === 'approved') return { label: '已通过', type: 'success' }
if (status === 'rejected') return { label: '已拒绝', type: 'danger' }
return { label: '待审核', type: 'warning' }
}
async function loadUsers() {
loading.value = true
try {
users.value = await fetchAllUsers()
} catch {
users.value = []
} finally {
loading.value = false
}
}
async function onApprove(row) {
try {
await ElMessageBox.confirm(`确定通过用户「${row.username}」的注册申请吗?`, '审核通过', {
confirmButtonText: '通过',
cancelButtonText: '取消',
type: 'success',
})
} catch {
return
}
try {
await approveUser(row.id)
ElMessage.success('用户审核通过')
await loadUsers()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onReject(row) {
try {
await ElMessageBox.confirm(`确定拒绝用户「${row.username}」的注册申请吗?`, '拒绝申请', {
confirmButtonText: '拒绝',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
await rejectUser(row.id)
ElMessage.success('已拒绝用户')
await loadUsers()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onDelete(row) {
try {
await ElMessageBox.confirm(
`确定删除用户「${row.username}」吗?此操作将删除该用户的所有数据,不可恢复!`,
'删除用户',
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'error' },
)
} catch {
return
}
try {
await deleteUser(row.id)
ElMessage.success('用户已删除')
await loadUsers()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onSetVip(row, days) {
const label = { 7: '一周', 30: '一个月', 365: '一年', 999999: '永久' }[days] || `${days}`
try {
await ElMessageBox.confirm(`确定为用户「${row.username}」开通 ${label} VIP 吗?`, '设置VIP', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await setUserVip(row.id, days)
ElMessage.success(res?.message || 'VIP设置成功')
await loadUsers()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onRemoveVip(row) {
try {
await ElMessageBox.confirm(`确定移除用户「${row.username}」的 VIP 吗?`, '移除VIP', {
confirmButtonText: '移除',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await removeUserVip(row.id)
ElMessage.success(res?.message || 'VIP已移除')
await loadUsers()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onResetPassword(row) {
let value
try {
const result = await ElMessageBox.prompt('请输入新密码至少8位且包含字母和数字', '重置密码', {
confirmButtonText: '提交',
cancelButtonText: '取消',
inputType: 'password',
inputPlaceholder: '新密码',
inputValidator: (v) => validatePasswordStrength(v).ok,
inputErrorMessage: '密码至少8位且包含字母和数字',
})
value = result.value
} catch {
return
}
const check = validatePasswordStrength(value)
if (!check.ok) {
ElMessage.error(check.message)
return
}
try {
await ElMessageBox.confirm(`确定将用户「${row.username}」的密码重置为该新密码吗?`, '二次确认', {
confirmButtonText: '确认重置',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await adminResetUserPassword(row.id, value)
ElMessage.success(res?.message || '密码重置成功')
} catch {
// handled by interceptor
}
}
onMounted(loadUsers)
</script>
<template>
<div class="page-stack">
<div class="app-page-title">
<h2>用户</h2>
<div>
<el-button @click="loadUsers">刷新</el-button>
</div>
</div>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<div class="table-wrap">
<el-table :data="users" v-loading="loading" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户" min-width="240">
<template #default="{ row }">
<div class="user-block">
<div class="user-main">
<strong>{{ row.username }}</strong>
<el-tag v-if="isVip(row)" type="warning" effect="light" size="small">VIP</el-tag>
</div>
<div v-if="row.email" class="app-muted user-sub">{{ row.email }}</div>
<div v-if="vipLabel(row)" class="vip-sub">{{ vipLabel(row) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="statusMeta(row.status).type" effect="light">{{ statusMeta(row.status).label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="时间" min-width="220">
<template #default="{ row }">
<div>{{ row.created_at }}</div>
<div v-if="row.approved_at" class="app-muted">审核: {{ row.approved_at }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<div class="actions">
<template v-if="row.status === 'pending'">
<el-button type="success" size="small" @click="onApprove(row)">通过</el-button>
<el-button type="warning" size="small" @click="onReject(row)">拒绝</el-button>
</template>
<el-dropdown trigger="click">
<el-button size="small">VIP</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="!isVip(row)" @click="onSetVip(row, 7)">开通一周</el-dropdown-item>
<el-dropdown-item v-if="!isVip(row)" @click="onSetVip(row, 30)">开通一月</el-dropdown-item>
<el-dropdown-item v-if="!isVip(row)" @click="onSetVip(row, 365)">开通一年</el-dropdown-item>
<el-dropdown-item v-if="!isVip(row)" @click="onSetVip(row, 999999)">永久VIP</el-dropdown-item>
<el-dropdown-item v-if="isVip(row)" @click="onRemoveVip(row)">移除VIP</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button size="small" @click="onResetPassword(row)">重置密码</el-button>
<el-button type="danger" size="small" @click="onDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
<style scoped>
.page-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.table-wrap {
overflow-x: auto;
}
.user-block {
display: flex;
flex-direction: column;
gap: 2px;
}
.user-main {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.user-sub {
font-size: 12px;
}
.vip-sub {
font-size: 12px;
color: #7c3aed;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>