Initial clean project import
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
<!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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Reference in New Issue
Block a user