Files
zsglpt/admin-frontend/src/pages/UsersPage.vue
Yu Yon 53c78e8e3c feat: 添加安全模块 + Dockerfile添加curl支持健康检查
主要更新:
- 新增 security/ 安全模块 (风险评估、威胁检测、蜜罐等)
- Dockerfile 添加 curl 以支持 Docker 健康检查
- 前端页面更新 (管理后台、用户端)
- 数据库迁移和 schema 更新
- 新增 kdocs 上传服务
- 添加安全相关测试用例

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 17:48:33 +08:00

339 lines
8.9 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,
deleteUser,
fetchAllUsers,
approveUser,
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 === 'rejected') return { label: '禁用', type: 'danger' }
return { label: '正常', type: 'success' }
}
async function loadUsers() {
loading.value = true
try {
users.value = await fetchAllUsers()
} catch {
users.value = []
} finally {
loading.value = false
}
}
async function refreshAll() {
await loadUsers()
}
async function onEnableUser(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 onDisableUser(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(refreshAll)
</script>
<template>
<div class="page-stack">
<div class="app-page-title">
<h2>用户</h2>
<div>
<el-button @click="refreshAll">刷新</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.vip_expire_time" class="app-muted">VIP到期: {{ row.vip_expire_time }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<div class="actions">
<el-button
v-if="row.status === 'rejected'"
type="success"
size="small"
@click="onEnableUser(row)"
>启用</el-button>
<el-button v-else type="warning" size="small" @click="onDisableUser(row)">禁用</el-button>
<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);
}
.section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 800;
}
.help {
margin-top: 10px;
font-size: 12px;
}
.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>