Download static/admin.html from harii66/gate: direct link, hf CLI and curl.
- Browser
- Download file 50 kB
-
https://huggingface.co/spaces/harii66/gate/resolve/main/static/admin.html
- Command line
-
hf download hf://spaces/harii66/gate/static/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/harii66/gate/resolve/main/static/admin.html
50 kB
| <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 ; | |
| } | |
| .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 ; | |
| } | |
| .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> |