李冠威 2 months ago
commit f496f5553e

@ -0,0 +1,816 @@
<template>
<div class="admin-container">
<h1>管理控制台</h1>
<div v-if="!isAdmin" class="admin-error">
<p>您没有权限访问此页面</p>
<router-link to="/" class="back-home">返回首页</router-link>
</div>
<div v-else class="admin-panel">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="{ 'active': activeTab === tab.id }">
{{ tab.name }}
</button>
</div>
<!-- 用户管理 -->
<div v-if="activeTab === 'users'" class="tab-content">
<h2>用户管理</h2>
<div class="search-bar">
<input
type="text"
v-model="userSearchTerm"
placeholder="搜索用户..."
@input="filterUsers"
/>
<button @click="refreshUsers" class="refresh-btn">刷新</button>
</div>
<div class="table-wrapper">
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>最高分</th>
<th>注册时间</th>
<th>最后登录</th>
<th>管理员</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.high_score }}</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>{{ formatDate(user.last_login) }}</td>
<td>
<input
type="checkbox"
:checked="user.is_admin"
@change="toggleAdminStatus(user)"
:disabled="currentUser && currentUser.id === user.id"
/>
</td>
<td class="actions">
<button @click="editUser(user)" class="edit-btn">编辑</button>
<button
@click="deleteUser(user)"
class="delete-btn"
:disabled="currentUser && currentUser.id === user.id">
删除
</button>
<button @click="resetUserScore(user.id)" class="reset-btn">
重置分数
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 用户编辑模态框 -->
<div v-if="showEditModal" class="modal-backdrop">
<div class="modal">
<h3>编辑用户</h3>
<form @submit.prevent="saveUserEdit">
<div class="form-group">
<label>用户名:</label>
<input type="text" v-model="editingUser.username" required />
</div>
<div class="form-group">
<label>最高分:</label>
<input type="number" v-model="editingUser.high_score" min="0" />
</div>
<div class="form-group">
<label>管理员权限:</label>
<input
type="checkbox"
v-model="editingUser.is_admin"
:disabled="currentUser && currentUser.id === editingUser.id"
/>
</div>
<div class="form-group">
<label>重设密码 (留空则不修改):</label>
<input type="password" v-model="editingUser.password" />
</div>
<div class="modal-actions">
<button type="submit" class="save-btn">保存</button>
<button type="button" @click="cancelEdit" class="cancel-btn">取消</button>
</div>
</form>
</div>
</div>
</div>
<!-- 排行榜管理 -->
<div v-if="activeTab === 'leaderboard'" class="tab-content">
<h2>排行榜管理</h2>
<div class="leaderboard-actions">
<button @click="confirmResetAllScores" class="danger-btn">
重置所有分数
</button>
</div>
<div class="table-wrapper">
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>用户名</th>
<th>最高分</th>
<th>最后游戏</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in leaderboard" :key="user.id">
<td>{{ index + 1 }}</td>
<td>{{ user.username }}</td>
<td>{{ user.high_score }}</td>
<td>{{ formatDate(user.last_login) }}</td>
<td class="actions">
<button @click="editUser(user)" class="edit-btn">编辑</button>
<button @click="resetUserScore(user.id)" class="reset-btn">
重置分数
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 游戏历史管理 -->
<div v-if="activeTab === 'history'" class="tab-content">
<h2>游戏历史管理</h2>
<div class="search-bar">
<input
type="text"
v-model="historySearchTerm"
placeholder="搜索用户..."
@input="filterGameHistory"
/>
<button @click="loadGameHistory" class="refresh-btn">刷新</button>
</div>
<div class="table-wrapper">
<table class="history-table">
<thead>
<tr>
<th>ID</th>
<th>用户</th>
<th>分数</th>
<th>等级</th>
<th>时长()</th>
<th>金币</th>
<th>游戏时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in filteredGameHistory" :key="record.id">
<td>{{ record.id }}</td>
<td>{{ record.username }}</td>
<td>{{ record.score }}</td>
<td>{{ record.level_reached }}</td>
<td>{{ record.duration }}</td>
<td>{{ record.gold_earned }}</td>
<td>{{ formatDate(record.created_at) }}</td>
<td class="actions">
<button @click="deleteGameRecord(record.id)" class="delete-btn">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 确认对话框 -->
<div v-if="showConfirmDialog" class="modal-backdrop">
<div class="modal confirm-dialog">
<h3>{{ confirmDialogTitle }}</h3>
<p>{{ confirmDialogMessage }}</p>
<div class="modal-actions">
<button @click="confirmDialogAction" class="danger-btn">确认</button>
<button @click="cancelConfirmDialog" class="cancel-btn">取消</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AdminPanel',
data() {
return {
isAdmin: false,
currentUser: null,
activeTab: 'users',
tabs: [
{ id: 'users', name: '用户管理' },
{ id: 'leaderboard', name: '排行榜管理' },
{ id: 'history', name: '游戏历史' }
],
//
users: [],
filteredUsers: [],
userSearchTerm: '',
//
leaderboard: [],
//
gameHistory: [],
filteredGameHistory: [],
historySearchTerm: '',
//
showEditModal: false,
editingUser: {
id: null,
username: '',
high_score: 0,
is_admin: false,
password: ''
},
//
showConfirmDialog: false,
confirmDialogTitle: '',
confirmDialogMessage: '',
confirmDialogAction: () => {}
}
},
created() {
this.checkAdminStatus()
.then(() => {
if (this.isAdmin) {
this.loadData()
}
})
},
methods: {
async checkAdminStatus() {
try {
const userData = JSON.parse(localStorage.getItem('user'))
const token = localStorage.getItem('auth_token')
console.log('检查管理员状态,当前用户数据:', userData)
console.log('当前令牌:', token ? token.substring(0, 8) + '...' : 'no token')
if (!userData) {
this.isAdmin = false
console.log('未登录,跳转到登录页面')
this.$router.push('/login')
return Promise.resolve(false)
}
//
this.isAdmin = userData.is_admin === true // true
console.log('本地存储显示用户管理员状态:', this.isAdmin)
//
console.log('开始向服务器验证管理员状态')
const response = await axios.get('/api/test/admin_status', {
withCredentials: true,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Authorization': token ? `Bearer ${token}` : ''
}
})
console.log('服务器返回的验证数据:', response.data)
if (response.data.logged_in && response.data.is_admin === true) {
console.log('服务器验证结果: 是管理员,允许访问管理页面')
this.isAdmin = true
this.currentUser = response.data.user_data
// localStorageis_admin
localStorage.setItem('user', JSON.stringify(response.data.user_data))
return Promise.resolve(true)
} else {
console.log('服务器验证结果: 不是管理员或未登录,跳转回首页')
this.isAdmin = false
//
if (!response.data.logged_in && userData) {
console.log('服务器会话可能已过期,尝试重新登录...')
this.$router.push('/login')
} else {
this.$router.push('/')
}
return Promise.resolve(false)
}
} catch (error) {
console.error('验证管理员状态失败:', error)
this.isAdmin = false
this.$router.push('/')
return Promise.resolve(false)
}
},
async loadData() {
if (!this.isAdmin) return
await Promise.all([
this.loadUsers(),
this.loadLeaderboard(),
this.loadGameHistory()
])
},
//
async loadUsers() {
try {
const response = await axios.get('/api/admin/users')
this.users = response.data.users
this.filterUsers()
} catch (error) {
console.error('加载用户列表失败:', error)
}
},
filterUsers() {
if (!this.userSearchTerm) {
this.filteredUsers = [...this.users]
return
}
const term = this.userSearchTerm.toLowerCase()
this.filteredUsers = this.users.filter(user =>
user.username.toLowerCase().includes(term) ||
user.id.toString().includes(term)
)
},
refreshUsers() {
this.loadUsers()
},
//
async loadLeaderboard() {
try {
const response = await axios.get('/api/leaderboard')
this.leaderboard = response.data.leaderboard
} catch (error) {
console.error('加载排行榜失败:', error)
}
},
async confirmResetAllScores() {
this.showConfirmDialog = true
this.confirmDialogTitle = '重置所有分数'
this.confirmDialogMessage = '确定要重置所有用户的分数吗?此操作不可撤销!'
this.confirmDialogAction = this.resetAllScores
},
async resetAllScores() {
try {
await axios.post('/api/admin/leaderboard/reset')
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('所有分数已重置')
} catch (error) {
console.error('重置分数失败:', error)
alert('重置分数失败: ' + (error.response?.data?.error || '未知错误'))
}
},
async resetUserScore(userId) {
this.showConfirmDialog = true
this.confirmDialogTitle = '重置用户分数'
this.confirmDialogMessage = '确定要重置此用户的分数吗?此操作不可撤销!'
this.confirmDialogAction = async () => {
try {
await axios.post('/api/admin/leaderboard/reset', { user_id: userId })
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('用户分数已重置')
} catch (error) {
console.error('重置分数失败:', error)
alert('重置分数失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
async loadGameHistory() {
try {
const response = await axios.get('/api/admin/game_history')
this.gameHistory = response.data.history
this.filterGameHistory()
} catch (error) {
console.error('加载游戏历史失败:', error)
}
},
filterGameHistory() {
if (!this.historySearchTerm) {
this.filteredGameHistory = [...this.gameHistory]
return
}
const term = this.historySearchTerm.toLowerCase()
this.filteredGameHistory = this.gameHistory.filter(record =>
record.username.toLowerCase().includes(term) ||
record.id.toString().includes(term)
)
},
async deleteGameRecord(recordId) {
this.showConfirmDialog = true
this.confirmDialogTitle = '删除游戏记录'
this.confirmDialogMessage = '确定要删除此游戏记录吗?此操作不可撤销!'
this.confirmDialogAction = async () => {
try {
await axios.delete(`/api/admin/game_history/${recordId}`)
this.showConfirmDialog = false
await this.loadGameHistory()
alert('记录已删除')
} catch (error) {
console.error('删除记录失败:', error)
alert('删除记录失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
editUser(user) {
this.editingUser = {
id: user.id,
username: user.username,
high_score: user.high_score,
is_admin: user.is_admin,
password: '' //
}
this.showEditModal = true
},
async saveUserEdit() {
try {
const payload = {
username: this.editingUser.username,
high_score: parseInt(this.editingUser.high_score),
is_admin: this.editingUser.is_admin
}
//
if (this.editingUser.password) {
payload.password = this.editingUser.password
}
await axios.put(`/api/admin/users/${this.editingUser.id}`, payload)
//
this.showEditModal = false
//
await this.loadUsers()
await this.loadLeaderboard()
// localStorage
const userData = JSON.parse(localStorage.getItem('user'))
if (userData && userData.id === this.editingUser.id) {
userData.username = this.editingUser.username
userData.high_score = this.editingUser.high_score
userData.is_admin = this.editingUser.is_admin
localStorage.setItem('user', JSON.stringify(userData))
}
alert('用户信息已更新')
} catch (error) {
console.error('更新用户失败:', error)
alert('更新用户失败: ' + (error.response?.data?.error || '未知错误'))
}
},
cancelEdit() {
this.showEditModal = false
this.editingUser = {
id: null,
username: '',
high_score: 0,
is_admin: false,
password: ''
}
},
async toggleAdminStatus(user) {
//
if (this.currentUser && this.currentUser.id === user.id) {
return
}
try {
await axios.put(`/api/admin/users/${user.id}`, {
is_admin: !user.is_admin
})
//
await this.loadUsers()
} catch (error) {
console.error('更新管理员状态失败:', error)
alert('更新管理员状态失败: ' + (error.response?.data?.error || '未知错误'))
}
},
async deleteUser(user) {
//
if (this.currentUser && this.currentUser.id === user.id) {
return
}
this.showConfirmDialog = true
this.confirmDialogTitle = '删除用户'
this.confirmDialogMessage = `确定要删除用户 "${user.username}" 吗?此操作将永久删除该用户及其所有数据,且不可撤销!`
this.confirmDialogAction = async () => {
try {
await axios.delete(`/api/admin/users/${user.id}`)
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('用户已删除')
} catch (error) {
console.error('删除用户失败:', error)
alert('删除用户失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
formatDate(dateString) {
if (!dateString) return '未知'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
},
cancelConfirmDialog() {
this.showConfirmDialog = false
this.confirmDialogTitle = ''
this.confirmDialogMessage = ''
this.confirmDialogAction = () => {}
}
}
}
</script>
<style scoped>
.admin-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #8B4513;
text-align: center;
margin-bottom: 30px;
}
.admin-error {
text-align: center;
padding: 50px 0;
}
.back-home {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #8B4513;
color: white;
text-decoration: none;
border-radius: 4px;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tabs button {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #555;
}
.tabs button.active {
color: #8B4513;
border-bottom: 3px solid #8B4513;
font-weight: bold;
}
.tab-content {
background: white;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tab-content h2 {
color: #8B4513;
margin-top: 0;
margin-bottom: 20px;
}
/* 表格样式 */
.table-wrapper {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f5f5f5;
color: #333;
}
tr:hover {
background-color: #f9f9f9;
}
/* 按钮样式 */
.actions {
display: flex;
gap: 5px;
}
.edit-btn, .delete-btn, .reset-btn, .refresh-btn {
padding: 5px 8px;
border: none;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.edit-btn {
background-color: #4CAF50;
color: white;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.reset-btn {
background-color: #ff9800;
color: white;
}
.refresh-btn {
background-color: #2196F3;
color: white;
margin-left: 10px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 搜索栏 */
.search-bar {
display: flex;
margin-bottom: 20px;
}
.search-bar input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 模态框 */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 20px;
border-radius: 5px;
width: 400px;
max-width: 90%;
}
.modal h3 {
margin-top: 0;
color: #8B4513;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.save-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.cancel-btn {
background-color: #9e9e9e;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.danger-btn {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
/* 确认对话框 */
.confirm-dialog {
width: 350px;
text-align: center;
}
.confirm-dialog p {
margin: 15px 0;
}
/* 排行榜操作 */
.leaderboard-actions {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,226 @@
<template>
<div class="admin-setup-container">
<div class="setup-form">
<h1>管理员初始化</h1>
<p class="description">请设置系统的第一个管理员账户此页面只能被访问一次</p>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="success" class="success-message">
{{ success }}
<div class="redirect-info">
即将跳转到登录页面... <span>{{ countdown }}</span>
</div>
</div>
<form @submit.prevent="setupAdmin" v-if="!success">
<div class="form-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
required
:disabled="loading"
placeholder="请输入用户名"
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
required
:disabled="loading"
placeholder="请输入密码"
/>
</div>
<div class="form-group">
<label for="setupKey">安装密钥</label>
<input
type="password"
id="setupKey"
v-model="setupKey"
required
:disabled="loading"
placeholder="请输入安装密钥"
/>
<small>此密钥用于确认您有权设置管理员账户</small>
</div>
<div class="form-actions">
<button type="submit" :disabled="loading">
{{ loading ? '处理中...' : '创建管理员账户' }}
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AdminSetup',
data() {
return {
username: '',
password: '',
setupKey: '',
loading: false,
error: '',
success: '',
countdown: 5
}
},
methods: {
async setupAdmin() {
this.loading = true
this.error = ''
try {
const response = await axios.post('/api/admin/setup', {
username: this.username,
password: this.password,
setup_key: this.setupKey
})
//
this.success = response.data.message
//
this.startRedirectCountdown()
} catch (error) {
console.error('设置管理员失败:', error)
this.error = error.response?.data?.error || '设置管理员失败,请稍后重试'
} finally {
this.loading = false
}
},
startRedirectCountdown() {
const timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(timer)
this.$router.push('/login')
}
}, 1000)
}
}
}
</script>
<style scoped>
.admin-setup-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
}
.setup-form {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
}
h1 {
color: #8B4513;
text-align: center;
margin-bottom: 10px;
}
.description {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
small {
display: block;
margin-top: 5px;
color: #777;
font-size: 12px;
}
.form-actions {
text-align: center;
margin-top: 30px;
}
button {
background-color: #8B4513;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #6d370f;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
}
.success-message {
background-color: #e8f5e9;
color: #2e7d32;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
}
.redirect-info {
margin-top: 15px;
font-size: 14px;
}
.redirect-info span {
font-weight: bold;
}
</style>
Loading…
Cancel
Save