|
|
|
@ -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
|
|
|
|
|
|
|
|
|
|
// 更新localStorage中的用户信息,确保is_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>
|