Files
WYF-koubo/activation-server/public/admin.html
T
2026-06-19 18:45:55 +08:00

319 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>激活码管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { background: #2c3e50; color: white; padding: 20px; border-radius: 4px; margin-bottom: 20px; }
.header h1 { font-size: 24px; margin-bottom: 10px; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; }
.stat { background: white; padding: 15px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-value { font-size: 24px; font-weight: bold; color: #2c3e50; }
.stat-label { font-size: 12px; color: #666; margin-top: 5px; }
.section { background: white; padding: 20px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.section h2 { font-size: 18px; margin-bottom: 15px; border-bottom: 2px solid #2c3e50; padding-bottom: 10px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px; }
.form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.form-group button { background: #3498db; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
.form-group button:hover { background: #2980b9; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 13px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; font-weight: bold; }
tr:hover { background: #f9f9f9; }
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; display: inline-block; }
.badge-active { background: #d4edda; color: #155724; }
.badge-inactive { background: #f8d7da; color: #721c24; }
.badge-used { background: #cfe2ff; color: #084298; }
.badge-unused { background: #e2e3e5; color: #383d41; }
.action-btn { padding: 4px 8px; margin: 0 2px; font-size: 12px; cursor: pointer; border: none; border-radius: 3px; color: white; font-weight: bold; }
.btn-disable { background: #ff7675; }
.btn-enable { background: #00b894; }
.btn-delete { background: #d63031; }
.btn-copy { background: #0984e3; }
.form-inline { display: grid; grid-template-columns: 1fr 100px 100px 100px 100px; gap: 10px; align-items: flex-end; }
.message { padding: 12px; border-radius: 4px; margin-bottom: 15px; display: none; font-weight: bold; }
.message.show { display: block; }
.message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 激活码管理后台 (开发版)</h1>
<p>本地激活服务器 - http://localhost:3001</p>
</div>
<div class="stats" id="stats">
<div class="stat"><div class="stat-value">0</div><div class="stat-label">总激活码数</div></div>
<div class="stat"><div class="stat-value">0</div><div class="stat-label">未使用</div></div>
<div class="stat"><div class="stat-value">0</div><div class="stat-label">已激活</div></div>
<div class="stat"><div class="stat-value">0</div><div class="stat-label">已停用</div></div>
</div>
<div class="section">
<h2>生成激活码</h2>
<div id="message" class="message"></div>
<div class="form-inline">
<div>
<label>过期类型:</label>
<select id="type" style="width: 100%;">
<option value="minute">分钟</option>
<option value="hour">小时</option>
<option value="day" selected></option>
<option value="month"></option>
<option value="year"></option>
</select>
</div>
<div style="grid-column: 2">
<label>数量:</label>
<input type="number" id="duration" value="1" min="1" style="width: 100%;">
</div>
<button onclick="generateCode()" style="width: 100%; padding: 8px; grid-column: 3">生成</button>
<button onclick="refreshList()" style="width: 100%; padding: 8px; background: #95a5a6; grid-column: 4">刷新</button>
<button onclick="location.reload()" style="width: 100%; padding: 8px; background: #7f8c8d; grid-column: 5">重载</button>
</div>
</div>
<div class="section">
<h2>激活码列表</h2>
<table id="codeTable">
<thead>
<tr>
<th>激活码</th>
<th>类型</th>
<th>过期时间</th>
<th>状态</th>
<th>激活</th>
<th>生成时间</th>
<th>最后启动时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="codeList">
<tr><td colspan="8" style="text-align: center; color: #999;">暂无激活码</td></tr>
</tbody>
</table>
</div>
</div>
<script>
const API_URL = 'http://localhost:3001/api'
async function generateCode() {
const type = document.getElementById('type').value
const duration = parseInt(document.getElementById('duration').value)
if (duration < 1) {
showMessage('数量必须大于0', 'error')
return
}
try {
const response = await fetch(`${API_URL}/codes/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, duration })
})
const data = await response.json()
if (data.success) {
showMessage(`✅ 激活码已生成: ${data.code}`, 'success')
document.getElementById('duration').value = '1'
setTimeout(refreshList, 500)
} else {
showMessage(`❌ 生成失败: ${data.error}`, 'error')
}
} catch (error) {
showMessage(`❌ 错误: ${error.message}`, 'error')
}
}
async function refreshList() {
try {
const response = await fetch(`${API_URL}/codes/list`)
const data = await response.json()
if (data.success) {
const tbody = document.getElementById('codeList')
tbody.innerHTML = ''
if (data.codes.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; color: #999;">暂无激活码</td></tr>'
return
}
data.codes.forEach(code => {
const row = document.createElement('tr')
const expiresAt = new Date(code.expires_at)
const now = new Date()
const isExpired = now > expiresAt
const statusBadge = code.is_active ?
'<span class="badge badge-active">✅ 可用</span>' :
'<span class="badge badge-inactive">❌ 已停用</span>'
const usedBadge = code.is_used ?
'<span class="badge badge-used">已激活</span>' :
'<span class="badge badge-unused">未使用</span>'
const typeLabel = { minute: '分钟', hour: '小时', day: '天', month: '月', year: '年' }[code.type] || code.type
const timeDisplay = isExpired ? `<span style="color: #d63031;">已过期</span>` : `<span style="color: #00b894;">${expiresAt.toLocaleString()}</span>`
// 最后启动时间
const lastCheckinDisplay = code.last_checkin ?
`<span style="color: #3498db; font-size: 12px;">${new Date(code.last_checkin).toLocaleString()}<br/>(${code.last_checkin_device || 'unknown'})</span>` :
'<span style="color: #999;">未启动</span>'
row.innerHTML = `
<td><code>${code.code}</code></td>
<td>${typeLabel} (${code.duration})</td>
<td>${timeDisplay}</td>
<td>${statusBadge}</td>
<td>${usedBadge}</td>
<td>${new Date(code.created_at).toLocaleString()}</td>
<td>${lastCheckinDisplay}</td>
<td>
<button class="action-btn btn-copy" onclick="copyCode('${code.code}')">复制</button>
${code.is_active ?
`<button class="action-btn btn-disable" onclick="disableCode('${code.code}')">停用</button>` :
`<button class="action-btn btn-enable" onclick="enableCode('${code.code}')">启用</button>`
}
<button class="action-btn btn-delete" onclick="deleteCode('${code.code}')">删除</button>
</td>
`
tbody.appendChild(row)
})
updateStats()
}
} catch (error) {
showMessage(`❌ 获取列表失败: ${error.message}`, 'error')
}
}
async function disableCode(code) {
try {
const response = await fetch(`${API_URL}/codes/disable`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
})
const data = await response.json()
if (data.success) {
showMessage(`✅ 激活码 ${code} 已停用`, 'success')
refreshList()
} else {
showMessage(`❌ 停用失败: ${data.error}`, 'error')
}
} catch (error) {
showMessage(`❌ 错误: ${error.message}`, 'error')
}
}
async function enableCode(code) {
try {
const response = await fetch(`${API_URL}/codes/enable`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
})
const data = await response.json()
if (data.success) {
showMessage(`✅ 激活码 ${code} 已启用`, 'success')
refreshList()
} else {
showMessage(`❌ 启用失败: ${data.error}`, 'error')
}
} catch (error) {
showMessage(`❌ 错误: ${error.message}`, 'error')
}
}
async function deleteCode(code) {
if (!confirm(`确定要删除激活码 ${code} 吗?`)) return
try {
const response = await fetch(`${API_URL}/codes/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
})
const data = await response.json()
if (data.success) {
showMessage(`✅ 激活码已删除`, 'success')
refreshList()
} else {
showMessage(`❌ 删除失败: ${data.error}`, 'error')
}
} catch (error) {
showMessage(`❌ 错误: ${error.message}`, 'error')
}
}
function copyCode(code) {
navigator.clipboard.writeText(code)
showMessage(`✅ 已复制: ${code}`, 'success')
}
async function updateStats() {
try {
const response = await fetch(`${API_URL}/codes/stats`)
const data = await response.json()
if (data.success) {
const stats = data.stats
const statsDiv = document.getElementById('stats')
statsDiv.innerHTML = `
<div class="stat">
<div class="stat-value">${stats.total || 0}</div>
<div class="stat-label">总激活码数</div>
</div>
<div class="stat">
<div class="stat-value">${stats.unused || 0}</div>
<div class="stat-label">未使用</div>
</div>
<div class="stat">
<div class="stat-value">${stats.used || 0}</div>
<div class="stat-label">已激活</div>
</div>
<div class="stat">
<div class="stat-value">${stats.disabled || 0}</div>
<div class="stat-label">已停用</div>
</div>
`
}
} catch (error) {
console.error('更新统计信息失败:', error)
}
}
function showMessage(message, type) {
const messageEl = document.getElementById('message')
messageEl.textContent = message
messageEl.className = `message show ${type}`
setTimeout(() => {
messageEl.classList.remove('show')
}, 3000)
}
// 页面加载时刷新列表
window.addEventListener('load', () => {
refreshList()
})
// 每 3 秒自动刷新一次列表(方便观察过期)
setInterval(refreshList, 3000)
</script>
</body>
</html>