gate / static /admin.html
harii66's picture
Upload 23 files
b4edbc0 verified
<!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>