| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>管理员后台 - Media Gateway</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); |
| |
| :root { |
| --primary: #6366f1; |
| --primary-dark: #4f46e5; |
| --primary-light: #818cf8; |
| --secondary: #ec4899; |
| --success: #10b981; |
| --warning: #f59e0b; |
| --danger: #ef4444; |
| --dark: #1e293b; |
| --light: #f8fafc; |
| --border: #e2e8f0; |
| --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| --gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%); |
| --gradient-warning: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); |
| --gradient-danger: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); |
| --shadow: 0 4px 15px rgba(0, 0, 0, 0.08); |
| --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.15); |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| background-size: 200% 200%; |
| animation: gradientShift 15s ease infinite; |
| background-attachment: fixed; |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| @keyframes gradientShift { |
| 0% { background-position: 0% 50%; } |
| 50% { background-position: 100% 50%; } |
| 100% { background-position: 0% 50%; } |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(20px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| @keyframes scaleIn { |
| from { opacity: 0; transform: scale(0.95); } |
| to { opacity: 1; transform: scale(1); } |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| .loading-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| z-index: 99999; |
| } |
| |
| .loading-overlay.hide { |
| display: none !important; |
| } |
| |
| .loading-spinner-large { |
| width: 60px; |
| height: 60px; |
| border: 5px solid rgba(255,255,255,.2); |
| border-radius: 50%; |
| border-top-color: #fff; |
| animation: spin 1s linear infinite; |
| } |
| |
| .loading-text { |
| color: white; |
| margin-top: 20px; |
| font-size: 1.2em; |
| font-weight: 600; |
| } |
| |
| .container { |
| max-width: 1600px; |
| margin: 0 auto; |
| background: rgba(255, 255, 255, 0.95); |
| backdrop-filter: blur(20px); |
| border-radius: 30px; |
| box-shadow: var(--shadow-lg); |
| overflow: hidden; |
| display: none; |
| animation: scaleIn 0.6s ease; |
| } |
| |
| .container.show { |
| display: block !important; |
| } |
| |
| .header { |
| background: var(--gradient-primary); |
| color: white; |
| padding: 30px 40px; |
| } |
| |
| .header-top { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 25px; |
| flex-wrap: wrap; |
| gap: 15px; |
| } |
| |
| .header h1 { |
| font-size: 2.5em; |
| margin-bottom: 5px; |
| font-weight: 800; |
| } |
| |
| .logout-btn { |
| padding: 12px 24px; |
| background: rgba(255, 255, 255, 0.2); |
| color: white; |
| border: 2px solid rgba(255, 255, 255, 0.5); |
| border-radius: 50px; |
| cursor: pointer; |
| font-size: 1em; |
| font-weight: 600; |
| } |
| |
| .logout-btn:hover { |
| background: rgba(255, 255, 255, 0.3); |
| } |
| |
| .stats-bar { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| gap: 20px; |
| padding: 30px 40px; |
| background: rgba(248, 250, 252, 0.8); |
| } |
| |
| .stat-card { |
| background: white; |
| padding: 28px; |
| border-radius: 20px; |
| box-shadow: var(--shadow); |
| text-align: center; |
| } |
| |
| .stat-card h3 { |
| color: #64748b; |
| font-size: 0.85em; |
| margin-bottom: 12px; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-weight: 700; |
| } |
| |
| .stat-card .number { |
| font-size: 2.5em; |
| font-weight: 800; |
| background: var(--gradient-primary); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .content { |
| padding: 40px; |
| } |
| |
| .section { |
| margin-bottom: 50px; |
| } |
| |
| .section h2 { |
| margin-bottom: 25px; |
| color: var(--dark); |
| font-size: 1.8em; |
| font-weight: 700; |
| } |
| |
| .form-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| margin-bottom: 25px; |
| } |
| |
| .form-group { |
| margin-bottom: 20px; |
| } |
| |
| .form-group label { |
| display: block; |
| margin-bottom: 8px; |
| font-weight: 600; |
| color: var(--dark); |
| } |
| |
| .form-group input, |
| .form-group select { |
| width: 100%; |
| padding: 14px 18px; |
| border: 2px solid var(--border); |
| border-radius: 12px; |
| font-size: 0.95em; |
| font-family: inherit; |
| } |
| |
| .form-group input:focus, |
| .form-group select:focus { |
| outline: none; |
| border-color: var(--primary); |
| } |
| |
| .btn { |
| padding: 12px 24px; |
| border: none; |
| border-radius: 12px; |
| cursor: pointer; |
| font-size: 0.95em; |
| font-weight: 600; |
| } |
| |
| .btn:hover:not(:disabled) { |
| opacity: 0.9; |
| } |
| |
| .btn:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| } |
| |
| .btn-primary { |
| background: var(--gradient-primary); |
| color: white; |
| } |
| |
| .btn-success { |
| background: var(--gradient-success); |
| color: white; |
| } |
| |
| .btn-warning { |
| background: var(--gradient-warning); |
| color: white; |
| } |
| |
| .btn-danger { |
| background: var(--gradient-danger); |
| color: white; |
| } |
| |
| .user-table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 20px; |
| background: white; |
| border-radius: 16px; |
| overflow: hidden; |
| box-shadow: var(--shadow); |
| } |
| |
| .user-table th, |
| .user-table td { |
| padding: 16px; |
| text-align: left; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| .user-table th { |
| background: var(--gradient-primary); |
| color: white; |
| font-weight: 700; |
| text-transform: uppercase; |
| font-size: 0.8em; |
| } |
| |
| .user-table tr:hover { |
| background: rgba(99, 102, 241, 0.05); |
| } |
| |
| .badge { |
| padding: 6px 14px; |
| border-radius: 20px; |
| font-size: 0.8em; |
| font-weight: 700; |
| display: inline-block; |
| } |
| |
| .badge-success { |
| background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); |
| color: #065f46; |
| } |
| |
| .badge-danger { |
| background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); |
| color: #991b1b; |
| } |
| |
| .badge-warning { |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); |
| color: #92400e; |
| } |
| |
| .badge-admin { |
| background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%); |
| color: white; |
| } |
| |
| .action-buttons { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| } |
| |
| .action-buttons .btn { |
| padding: 8px 14px; |
| font-size: 0.85em; |
| } |
| |
| .user-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 5px; |
| padding: 4px 10px; |
| border-radius: 12px; |
| font-size: 0.8em; |
| font-weight: 700; |
| } |
| |
| .badge-selector { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
| gap: 12px; |
| margin-top: 15px; |
| } |
| |
| .badge-option { |
| padding: 12px; |
| border: 2px solid var(--border); |
| border-radius: 12px; |
| cursor: pointer; |
| text-align: center; |
| transition: all 0.3s ease; |
| } |
| |
| .badge-option:hover { |
| transform: translateY(-3px); |
| } |
| |
| .badge-option.selected { |
| border-color: var(--primary); |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); |
| } |
| |
| .badge-preview { |
| display: inline-flex; |
| align-items: center; |
| gap: 5px; |
| padding: 6px 12px; |
| border-radius: 12px; |
| font-size: 0.85em; |
| font-weight: 700; |
| margin-top: 8px; |
| } |
| |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(30, 41, 59, 0.8); |
| z-index: 9999; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .modal.show { |
| display: flex; |
| } |
| |
| .modal-content { |
| background: white; |
| padding: 35px; |
| border-radius: 24px; |
| max-width: 500px; |
| width: 90%; |
| max-height: 85vh; |
| overflow-y: auto; |
| } |
| |
| .modal-header h2 { |
| color: var(--dark); |
| font-size: 1.8em; |
| font-weight: 700; |
| margin-bottom: 24px; |
| } |
| |
| .modal-footer { |
| margin-top: 24px; |
| display: flex; |
| gap: 12px; |
| justify-content: flex-end; |
| } |
| |
| .password-display { |
| background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); |
| padding: 24px; |
| border-radius: 16px; |
| border: 2px dashed #3b82f6; |
| margin: 20px 0; |
| text-align: center; |
| } |
| |
| .password-display .password { |
| font-size: 1.6em; |
| font-weight: 800; |
| color: #1e40af; |
| font-family: 'Courier New', monospace; |
| margin: 16px 0; |
| padding: 16px; |
| background: white; |
| border-radius: 12px; |
| } |
| |
| .notification { |
| position: fixed; |
| top: 30px; |
| right: 30px; |
| padding: 18px 28px; |
| border-radius: 16px; |
| z-index: 10000; |
| color: white; |
| font-weight: 600; |
| } |
| |
| .notification.success { |
| background: linear-gradient(135deg, #10b981 0%, #059669 100%); |
| } |
| |
| .notification.error { |
| background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); |
| } |
| |
| @media (max-width: 768px) { |
| .form-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="loading-overlay" id="loadingOverlay"> |
| <div class="loading-spinner-large"></div> |
| <div class="loading-text">验证身份中...</div> |
| </div> |
|
|
| <div class="container" id="mainContainer"> |
| <header class="header"> |
| <div class="header-top"> |
| <div> |
| <h1>🔐 管理员后台</h1> |
| <p>Media Gateway - User Management System</p> |
| </div> |
| <button class="logout-btn" id="logoutBtn">🚪 退出登录</button> |
| </div> |
| </header> |
|
|
| <div class="stats-bar"> |
| <div class="stat-card"> |
| <h3>总用户数</h3> |
| <div class="number" id="totalUsers">0</div> |
| </div> |
| <div class="stat-card"> |
| <h3>活跃用户</h3> |
| <div class="number" id="activeUsers">0</div> |
| </div> |
| <div class="stat-card"> |
| <h3>已过期</h3> |
| <div class="number" id="expiredUsers">0</div> |
| </div> |
| <div class="stat-card"> |
| <h3>已禁用</h3> |
| <div class="number" id="inactiveUsers">0</div> |
| </div> |
| </div> |
|
|
| <div class="content"> |
| <div class="section"> |
| <h2>➕ 创建新用户</h2> |
| <form id="createUserForm"> |
| <div class="form-grid"> |
| <div class="form-group"> |
| <label for="username">👤 用户名 *</label> |
| <input type="text" id="username" placeholder="请输入用户名" required> |
| </div> |
| <div class="form-group"> |
| <label for="password">🔒 密码(留空自动生成)</label> |
| <input type="text" id="password" placeholder="留空则自动生成强密码"> |
| </div> |
| <div class="form-group"> |
| <label for="expiryDays">⏰ 有效期(天)</label> |
| <select id="expiryDays"> |
| <option value="">永久有效</option> |
| <option value="7">7天</option> |
| <option value="30" selected>30天</option> |
| <option value="90">90天</option> |
| <option value="180">180天</option> |
| <option value="365">365天</option> |
| <option value="custom">自定义天数</option> |
| </select> |
| </div> |
| <div class="form-group" id="customDaysGroup" style="display: none;"> |
| <label for="customDays">🔢 自定义天数</label> |
| <input type="number" id="customDays" placeholder="输入天数" min="1"> |
| </div> |
| |
| <div class="form-group"> |
| <label for="userType">👑 用户类型</label> |
| <select id="userType"> |
| <option value="user" selected>👤 普通用户</option> |
| <option value="admin">👑 管理员</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label for="notes">📝 备注信息</label> |
| <input type="text" id="notes" placeholder="可选,添加备注信息"> |
| </div> |
| </div> |
| |
| <div class="form-group"> |
| <label>🏷️ 用户徽章(可选)</label> |
| <div class="badge-selector" id="badgeSelector"> |
| <div class="badge-option selected" data-badge="" onclick="selectBadge('')"> |
| <div style="font-size: 2em;">❌</div> |
| <div style="margin-top: 5px; font-size: 0.85em;">无徽章</div> |
| </div> |
| </div> |
| <input type="hidden" id="selectedBadge" value=""> |
| </div> |
| |
| <button type="submit" class="btn btn-primary" style="width: 100%; padding: 16px;"> |
| ➕ 创建用户 |
| </button> |
| </form> |
| </div> |
|
|
| <div class="section"> |
| <h2>👥 用户列表</h2> |
| <button class="btn btn-success" onclick="loadUsers()" style="margin-bottom: 20px;"> |
| 🔄 刷新列表 |
| </button> |
| <div style="overflow-x: auto;"> |
| <table class="user-table"> |
| <thead> |
| <tr> |
| <th>用户名</th> |
| <th>类型</th> |
| <th>徽章</th> |
| <th>创建时间</th> |
| <th>过期时间</th> |
| <th>最后登录</th> |
| <th>状态</th> |
| <th>备注</th> |
| <th>操作</th> |
| </tr> |
| </thead> |
| <tbody id="userTableBody"> |
| <tr> |
| <td colspan="9" style="text-align: center; padding: 40px;"> |
| <div class="loading-spinner-large" style="margin: 0 auto 15px;"></div> |
| <p>加载用户列表中...</p> |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="modal" id="passwordModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>✅ 用户创建成功</h2> |
| </div> |
| <div class="password-display"> |
| <p><strong>用户名:</strong><span id="displayUsername"></span></p> |
| <p><strong>登录密码:</strong></p> |
| <div class="password" id="displayPassword"></div> |
| <button class="btn btn-primary" onclick="copyPassword()"> |
| 📋 复制密码 |
| </button> |
| </div> |
| <p style="color: var(--danger); font-weight: 700; margin-top: 16px;"> |
| ⚠️ 请务必保存此密码!关闭后将无法再次查看。 |
| </p> |
| <div class="modal-footer"> |
| <button class="btn btn-primary" onclick="closePasswordModal()"> |
| 我已保存密码 |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="modal" id="badgeModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>🏷️ 设置用户徽章</h2> |
| </div> |
| <p style="margin-bottom: 20px; color: #64748b;"> |
| 为用户 <strong id="badgeUsername"></strong> 选择徽章 |
| </p> |
| <div class="badge-selector" id="badgeSelectorModal"></div> |
| <div class="modal-footer"> |
| <button class="btn btn-primary" onclick="confirmBadge()"> |
| ✅ 确认设置 |
| </button> |
| <button class="btn" onclick="closeBadgeModal()" style="background: #94a3b8; color: white;"> |
| 取消 |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| (function() { |
| 'use strict'; |
| |
| const API = window.location.origin; |
| let availableBadges = {}; |
| let currentBadgeUsername = ''; |
| let selectedModalBadge = ''; |
| |
| function getAdminToken() { |
| const token = localStorage.getItem('admin_token'); |
| return token; |
| } |
| |
| function getTokenExpiry() { |
| const expiry = localStorage.getItem('admin_token_expiry'); |
| return expiry ? parseInt(expiry) : 0; |
| } |
| |
| function clearAuth() { |
| localStorage.removeItem('admin_token'); |
| localStorage.removeItem('admin_token_expiry'); |
| localStorage.removeItem('admin_username'); |
| } |
| |
| async function checkAuth() { |
| const token = getAdminToken(); |
| const expiry = getTokenExpiry(); |
| |
| if (!token) { |
| return false; |
| } |
| |
| if (Date.now() > expiry) { |
| clearAuth(); |
| return false; |
| } |
| |
| try { |
| const response = await fetch(`${API}/api/admin/check`, { |
| method: 'GET', |
| headers: { |
| 'Authorization': `Bearer ${token}` |
| } |
| }); |
| |
| if (!response.ok) { |
| clearAuth(); |
| return false; |
| } |
| |
| const data = await response.json(); |
| |
| if (data.authenticated === true) { |
| return true; |
| } |
| |
| clearAuth(); |
| return false; |
| |
| } catch (error) { |
| clearAuth(); |
| return false; |
| } |
| } |
| |
| function redirectToLogin() { |
| setTimeout(() => { |
| window.location.href = '/admin/login'; |
| }, 1000); |
| } |
| |
| function logout() { |
| if (confirm('确定要退出登录吗?')) { |
| clearAuth(); |
| redirectToLogin(); |
| } |
| } |
| |
| function showNotification(message, type = 'success') { |
| const notification = document.createElement('div'); |
| notification.className = `notification ${type}`; |
| notification.textContent = message; |
| document.body.appendChild(notification); |
| |
| setTimeout(() => { |
| notification.remove(); |
| }, 3000); |
| } |
| |
| function showLoading(text = '验证身份中...') { |
| const overlay = document.getElementById('loadingOverlay'); |
| const loadingText = overlay?.querySelector('.loading-text'); |
| if (overlay) { |
| overlay.classList.remove('hide'); |
| if (loadingText) loadingText.textContent = text; |
| } |
| } |
| |
| function hideLoading() { |
| const overlay = document.getElementById('loadingOverlay'); |
| if (overlay) overlay.classList.add('hide'); |
| |
| const container = document.getElementById('mainContainer'); |
| if (container) container.classList.add('show'); |
| } |
| |
| async function loadBadges() { |
| try { |
| const token = getAdminToken(); |
| const response = await fetch(`${API}/api/admin/badges`, { |
| headers: { 'Authorization': `Bearer ${token}` } |
| }); |
| |
| if (response.ok) { |
| const data = await response.json(); |
| availableBadges = data.badges || {}; |
| renderBadgeSelector(); |
| } |
| } catch (error) { |
| } |
| } |
| |
| function renderBadgeSelector() { |
| const selector = document.getElementById('badgeSelector'); |
| if (!selector) return; |
| |
| let html = ` |
| <div class="badge-option selected" data-badge="" onclick="selectBadge('')"> |
| <div style="font-size: 2em;">❌</div> |
| <div style="margin-top: 5px; font-size: 0.85em;">无徽章</div> |
| </div> |
| `; |
| |
| for (const [id, badge] of Object.entries(availableBadges)) { |
| html += ` |
| <div class="badge-option" data-badge="${id}" onclick="selectBadge('${id}')"> |
| <div style="font-size: 2em;">${badge.icon}</div> |
| <div class="badge-preview" style=" |
| background: ${badge.gradient}; |
| color: ${badge.color}; |
| border: 2px solid ${badge.border}; |
| box-shadow: 0 2px 8px ${badge.glow}; |
| "> |
| ${badge.name} |
| </div> |
| </div> |
| `; |
| } |
| |
| selector.innerHTML = html; |
| } |
| |
| window.selectBadge = function(badgeId) { |
| const options = document.querySelectorAll('#badgeSelector .badge-option'); |
| options.forEach(opt => { |
| opt.classList.toggle('selected', opt.dataset.badge === badgeId); |
| }); |
| document.getElementById('selectedBadge').value = badgeId; |
| }; |
| |
| window.openBadgeModal = function(username) { |
| currentBadgeUsername = username; |
| document.getElementById('badgeUsername').textContent = username; |
| |
| const modalSelector = document.getElementById('badgeSelectorModal'); |
| if (!modalSelector) return; |
| |
| let html = ` |
| <div class="badge-option selected" data-badge="" onclick="selectModalBadge('')"> |
| <div style="font-size: 2em;">❌</div> |
| <div style="margin-top: 5px; font-size: 0.85em;">无徽章</div> |
| </div> |
| `; |
| |
| for (const [id, badge] of Object.entries(availableBadges)) { |
| html += ` |
| <div class="badge-option" data-badge="${id}" onclick="selectModalBadge('${id}')"> |
| <div style="font-size: 2em;">${badge.icon}</div> |
| <div class="badge-preview" style=" |
| background: ${badge.gradient}; |
| color: ${badge.color}; |
| border: 2px solid ${badge.border}; |
| box-shadow: 0 2px 8px ${badge.glow}; |
| "> |
| ${badge.name} |
| </div> |
| </div> |
| `; |
| } |
| |
| modalSelector.innerHTML = html; |
| selectedModalBadge = ''; |
| document.getElementById('badgeModal').classList.add('show'); |
| }; |
| |
| window.selectModalBadge = function(badgeId) { |
| const options = document.querySelectorAll('#badgeSelectorModal .badge-option'); |
| options.forEach(opt => { |
| opt.classList.toggle('selected', opt.dataset.badge === badgeId); |
| }); |
| selectedModalBadge = badgeId; |
| }; |
| |
| window.confirmBadge = async function() { |
| if (!currentBadgeUsername) { |
| showNotification('⚠️ 未选择用户', 'error'); |
| return; |
| } |
| |
| const token = getAdminToken(); |
| |
| if (!token) { |
| showNotification('⚠️ 未登录,请重新登录', 'error'); |
| setTimeout(() => { |
| window.location.href = '/admin/login'; |
| }, 1500); |
| return; |
| } |
| |
| try { |
| const response = await fetch(`${API}/api/admin/users/${currentBadgeUsername}/badge`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| badge: selectedModalBadge || null |
| }) |
| }); |
| |
| if (response.status === 401) { |
| clearAuth(); |
| showNotification('❌ 登录已过期,请重新登录', 'error'); |
| setTimeout(() => { |
| window.location.href = '/admin/login'; |
| }, 1500); |
| return; |
| } |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || '设置失败'); |
| } |
| |
| const data = await response.json(); |
| |
| showNotification('✅ 徽章设置成功', 'success'); |
| closeBadgeModal(); |
| await loadUsers(); |
| } catch (error) { |
| showNotification('❌ 设置徽章失败: ' + error.message, 'error'); |
| } |
| }; |
| |
| window.closeBadgeModal = function() { |
| document.getElementById('badgeModal').classList.remove('show'); |
| currentBadgeUsername = ''; |
| selectedModalBadge = ''; |
| }; |
| |
| async function loadStats() { |
| const token = getAdminToken(); |
| |
| try { |
| const response = await fetch(`${API}/api/admin/stats`, { |
| headers: { 'Authorization': `Bearer ${token}` } |
| }); |
| |
| if (response.ok) { |
| const data = await response.json(); |
| document.getElementById('totalUsers').textContent = data.total || 0; |
| document.getElementById('activeUsers').textContent = data.active || 0; |
| document.getElementById('expiredUsers').textContent = data.expired || 0; |
| document.getElementById('inactiveUsers').textContent = data.inactive || 0; |
| } |
| } catch (error) { |
| } |
| } |
| |
| async function loadUsers() { |
| const token = getAdminToken(); |
| const tbody = document.getElementById('userTableBody'); |
| |
| tbody.innerHTML = ` |
| <tr> |
| <td colspan="9" style="text-align: center; padding: 40px;"> |
| <div class="loading-spinner-large" style="margin: 0 auto 15px;"></div> |
| <p>加载中...</p> |
| </td> |
| </tr> |
| `; |
| |
| try { |
| const response = await fetch(`${API}/api/admin/users`, { |
| headers: { 'Authorization': `Bearer ${token}` } |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`HTTP ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| |
| renderUsers(data.users); |
| await loadStats(); |
| showNotification('✅ 用户列表已刷新', 'success'); |
| } catch (error) { |
| tbody.innerHTML = ` |
| <tr> |
| <td colspan="9" style="text-align: center; padding: 40px; color: #ef4444;"> |
| ❌ 加载失败: ${error.message} |
| </td> |
| </tr> |
| `; |
| showNotification('❌ 加载用户列表失败', 'error'); |
| } |
| } |
| |
| function renderUsers(users) { |
| const tbody = document.getElementById('userTableBody'); |
| |
| if (!users || users.length === 0) { |
| tbody.innerHTML = ` |
| <tr> |
| <td colspan="9" style="text-align: center; padding: 40px;"> |
| <div style="font-size: 3em; margin-bottom: 15px;">👥</div> |
| <p style="font-weight: 600;">暂无用户</p> |
| </td> |
| </tr> |
| `; |
| return; |
| } |
| |
| tbody.innerHTML = users.map(user => { |
| const createdAt = new Date(user.created_at).toLocaleString('zh-CN'); |
| const expiresAt = user.expires_at ? new Date(user.expires_at).toLocaleString('zh-CN') : '永久'; |
| const lastLogin = user.last_login ? new Date(user.last_login).toLocaleString('zh-CN') : '从未登录'; |
| |
| |
| const userTypeBadge = user.is_admin |
| ? '<span class="badge badge-admin">👑 管理员</span>' |
| : '<span class="badge badge-success">👤 普通用户</span>'; |
| |
| let statusBadge = ''; |
| if (!user.is_active) { |
| statusBadge = '<span class="badge badge-danger">已禁用</span>'; |
| } else if (user.expires_at && new Date(user.expires_at) < new Date()) { |
| statusBadge = '<span class="badge badge-warning">已过期</span>'; |
| } else { |
| statusBadge = '<span class="badge badge-success">正常</span>'; |
| } |
| |
| let userBadgeHtml = '-'; |
| if (user.badge && availableBadges[user.badge]) { |
| const badge = availableBadges[user.badge]; |
| userBadgeHtml = ` |
| <div class="user-badge" style=" |
| background: ${badge.gradient}; |
| color: ${badge.color}; |
| border: 2px solid ${badge.border}; |
| box-shadow: 0 2px 8px ${badge.glow}; |
| "> |
| <span>${badge.icon}</span> |
| <span>${badge.name}</span> |
| </div> |
| `; |
| } |
| |
| return ` |
| <tr> |
| <td><strong>${user.username}</strong></td> |
| <td>${userTypeBadge}</td> |
| <td>${userBadgeHtml}</td> |
| <td>${createdAt}</td> |
| <td>${expiresAt}</td> |
| <td>${lastLogin}</td> |
| <td>${statusBadge}</td> |
| <td>${user.notes || '-'}</td> |
| <td> |
| <div class="action-buttons"> |
| <button class="btn btn-primary" onclick="openBadgeModal('${user.username}')">🏷️</button> |
| ${user.is_active ? |
| `<button class="btn btn-warning" onclick="toggleUser('${user.username}', false)">禁用</button>` : |
| `<button class="btn btn-success" onclick="toggleUser('${user.username}', true)">启用</button>` |
| } |
| <button class="btn btn-primary" onclick="extendExpiry('${user.username}')">续期</button> |
| <button class="btn btn-danger" onclick="deleteUser('${user.username}')">删除</button> |
| </div> |
| </td> |
| </tr> |
| `; |
| }).join(''); |
| } |
| |
| async function createUser(username, password, expiryDays, notes, badge, isAdmin) { |
| const token = getAdminToken(); |
| |
| const response = await fetch(`${API}/api/admin/users`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| username, |
| password: password || null, |
| expires_days: expiryDays ? parseInt(expiryDays) : null, |
| notes, |
| badge: badge || null, |
| is_admin: isAdmin |
| }) |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json(); |
| throw new Error(error.error || '创建失败'); |
| } |
| |
| return await response.json(); |
| } |
| |
| window.toggleUser = async function(username, activate) { |
| const token = getAdminToken(); |
| |
| if (!token) { |
| showNotification('⚠️ 未登录,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| const action = activate ? 'activate' : 'deactivate'; |
| const actionText = activate ? '启用' : '禁用'; |
| |
| if (!confirm(`确定要${actionText}用户 ${username} 吗?`)) return; |
| |
| try { |
| const response = await fetch(`${API}/api/admin/users/${username}/${action}`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| } |
| }); |
| |
| if (response.status === 401) { |
| clearAuth(); |
| showNotification('❌ 登录已过期,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| if (response.ok) { |
| showNotification(`✅ 用户已${actionText}`, 'success'); |
| await loadUsers(); |
| } else { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || `${actionText}失败`); |
| } |
| } catch (error) { |
| showNotification(`❌ ${error.message}`, 'error'); |
| } |
| }; |
| |
| window.deleteUser = async function(username) { |
| if (!confirm(`⚠️ 确定要删除用户 ${username} 吗?\n\n此操作不可恢复!`)) return; |
| |
| const token = getAdminToken(); |
| |
| if (!token) { |
| showNotification('⚠️ 未登录,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| try { |
| const response = await fetch(`${API}/api/admin/users/${username}`, { |
| method: 'DELETE', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| } |
| }); |
| |
| if (response.status === 401) { |
| clearAuth(); |
| showNotification('❌ 登录已过期,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| if (response.ok) { |
| showNotification('✅ 用户已删除', 'success'); |
| await loadUsers(); |
| } else { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || '删除失败'); |
| } |
| } catch (error) { |
| showNotification(`❌ ${error.message}`, 'error'); |
| } |
| }; |
| |
| window.extendExpiry = async function(username) { |
| const days = prompt('请输入要延长的天数:', '30'); |
| if (!days || isNaN(days) || parseInt(days) <= 0) return; |
| |
| const token = getAdminToken(); |
| |
| if (!token) { |
| showNotification('⚠️ 未登录,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| try { |
| const response = await fetch(`${API}/api/admin/users/${username}/extend`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ days: parseInt(days) }) |
| }); |
| |
| if (response.status === 401) { |
| clearAuth(); |
| showNotification('❌ 登录已过期,请重新登录', 'error'); |
| setTimeout(() => window.location.href = '/admin/login', 1500); |
| return; |
| } |
| |
| if (response.ok) { |
| showNotification(`✅ 已为用户 ${username} 延长 ${days} 天`, 'success'); |
| await loadUsers(); |
| } else { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || '续期失败'); |
| } |
| } catch (error) { |
| showNotification(`❌ ${error.message}`, 'error'); |
| } |
| }; |
| |
| window.copyPassword = function() { |
| const password = document.getElementById('displayPassword').textContent; |
| navigator.clipboard.writeText(password).then(() => { |
| showNotification('✅ 密码已复制', 'success'); |
| }).catch(() => { |
| showNotification('❌ 复制失败', 'error'); |
| }); |
| }; |
| |
| window.closePasswordModal = function() { |
| document.getElementById('passwordModal').classList.remove('show'); |
| }; |
| |
| window.loadUsers = loadUsers; |
| |
| function handleCreateUserForm() { |
| const form = document.getElementById('createUserForm'); |
| if (!form) return; |
| |
| |
| const expiryDays = document.getElementById('expiryDays'); |
| const customDaysGroup = document.getElementById('customDaysGroup'); |
| |
| if (expiryDays) { |
| expiryDays.addEventListener('change', (e) => { |
| if (e.target.value === 'custom') { |
| customDaysGroup.style.display = 'block'; |
| } else { |
| customDaysGroup.style.display = 'none'; |
| } |
| }); |
| } |
| |
| form.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| |
| const username = document.getElementById('username').value.trim(); |
| const password = document.getElementById('password').value.trim(); |
| let expiryDaysValue = document.getElementById('expiryDays').value; |
| const notes = document.getElementById('notes').value.trim(); |
| const badge = document.getElementById('selectedBadge').value; |
| const userType = document.getElementById('userType').value; |
| |
| |
| if (expiryDaysValue === 'custom') { |
| const customDays = document.getElementById('customDays').value; |
| if (!customDays || parseInt(customDays) <= 0) { |
| showNotification('⚠️ 请输入有效的自定义天数', 'error'); |
| return; |
| } |
| expiryDaysValue = customDays; |
| } |
| |
| if (!username) { |
| showNotification('⚠️ 请输入用户名', 'error'); |
| return; |
| } |
| |
| const submitBtn = form.querySelector('button[type="submit"]'); |
| const originalText = submitBtn.textContent; |
| submitBtn.disabled = true; |
| submitBtn.textContent = '创建中...'; |
| |
| try { |
| const isAdmin = userType === 'admin'; |
| const data = await createUser(username, password, expiryDaysValue, notes, badge, isAdmin); |
| |
| document.getElementById('displayUsername').textContent = username; |
| document.getElementById('displayPassword').textContent = data.password; |
| document.getElementById('passwordModal').classList.add('show'); |
| |
| form.reset(); |
| selectBadge(''); |
| |
| await loadUsers(); |
| showNotification('✅ 用户创建成功', 'success'); |
| } catch (error) { |
| showNotification(`❌ 创建失败: ${error.message}`, 'error'); |
| } finally { |
| submitBtn.disabled = false; |
| submitBtn.textContent = originalText; |
| } |
| }); |
| } |
| |
| async function initialize() { |
| showLoading('验证身份中...'); |
| |
| try { |
| const isAuthenticated = await checkAuth(); |
| |
| if (!isAuthenticated) { |
| showLoading('未登录,正在跳转...'); |
| redirectToLogin(); |
| return; |
| } |
| |
| const logoutBtn = document.getElementById('logoutBtn'); |
| if (logoutBtn) { |
| logoutBtn.addEventListener('click', logout); |
| } |
| |
| handleCreateUserForm(); |
| |
| showLoading('加载数据...'); |
| |
| await loadBadges(); |
| await loadUsers(); |
| |
| hideLoading(); |
| |
| } catch (error) { |
| showLoading('初始化失败...'); |
| setTimeout(() => { |
| window.location.reload(); |
| }, 2000); |
| } |
| } |
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', initialize); |
| } else { |
| initialize(); |
| } |
| |
| })(); |
| </script> |
| </body> |
| </html> |