| <!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> |