feat: 新增平台

This commit is contained in:
2025-11-30 19:49:25 +08:00
parent c3e56a954d
commit fbd2c491b2
41 changed files with 4293 additions and 76 deletions

View File

@@ -0,0 +1,521 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>兑换码管理 - 管理后台</title>
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
<link rel="stylesheet" href="/static/css/admin.css">
</head>
<body class="admin-layout">
<header class="admin-header">
<div class="header-container">
<a href="/admin/dashboard" class="brand">
<span style="font-size: 1.5rem;"></span> JieXi Admin
</a>
<nav class="nav-links">
<a href="/admin/dashboard" class="nav-item">仪表板</a>
<a href="/admin/users" class="nav-item">用户管理</a>
<a href="/admin/apis" class="nav-item">接口管理</a>
<a href="/admin/redeem-codes" class="nav-item active">兑换码</a>
<a href="/admin/config" class="nav-item">系统配置</a>
<a href="/admin/logs" class="nav-item">日志审计</a>
</nav>
<div class="user-actions">
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
</div>
</div>
</header>
<main class="main-container">
<div class="page-header">
<h1 class="page-title">兑换码管理</h1>
<button class="ui-btn ui-btn-primary" onclick="showGenerateModal()">批量生成</button>
</div>
<!-- 统计卡片 -->
<div class="stats-grid" style="margin-bottom: 1.5rem;">
<div class="stat-card">
<div class="stat-label">总数量</div>
<div class="stat-value" id="totalCount">-</div>
</div>
<div class="stat-card">
<div class="stat-label">未使用</div>
<div class="stat-value" id="unusedCount" style="color: var(--success);">-</div>
</div>
<div class="stat-card">
<div class="stat-label">已使用</div>
<div class="stat-value" id="usedCount">-</div>
</div>
</div>
<!-- 筛选 -->
<div class="filter-bar" style="display: flex; gap: 1rem; margin-bottom: 1rem;">
<select id="filterStatus" class="ui-select" onchange="loadCodes()">
<option value="">全部状态</option>
<option value="unused">未使用</option>
<option value="used">已使用</option>
<option value="expired">已过期</option>
</select>
<select id="filterBatch" class="ui-select" onchange="loadCodes()">
<option value="">全部批次</option>
</select>
</div>
<!-- 兑换码列表 -->
<div class="ui-card">
<div class="table-container">
<table>
<thead>
<tr>
<th>兑换码</th>
<th>目标套餐</th>
<th>有效期</th>
<th>状态</th>
<th>使用者</th>
<th>使用时间</th>
<th>过期时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="codesBody">
<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 1rem;"></div>
</div>
</main>
<!-- 生成兑换码弹窗 -->
<div class="modal" id="generateModal">
<div class="modal-content">
<div class="modal-header">
<h3>批量生成兑换码</h3>
<button class="modal-close" onclick="closeModal('generateModal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>目标套餐 <span class="required">*</span></label>
<select id="targetGroup" class="ui-input">
<option value="">请选择</option>
</select>
</div>
<div class="form-group">
<label>套餐有效期(天)</label>
<input type="number" id="durationDays" class="ui-input" value="30" min="1">
<small>用户兑换后,套餐的有效天数</small>
</div>
<div class="form-group">
<label>生成数量</label>
<input type="number" id="generateCount" class="ui-input" value="10" min="1" max="1000">
</div>
<div class="form-group">
<label>兑换码前缀(可选)</label>
<input type="text" id="codePrefix" class="ui-input" placeholder="如: VIP">
</div>
<div class="form-group">
<label>兑换码有效期(天,可选)</label>
<input type="number" id="expiresDays" class="ui-input" placeholder="留空表示永不过期">
<small>兑换码本身的过期时间,过期后无法使用</small>
</div>
<div class="form-group">
<label>备注(可选)</label>
<input type="text" id="remark" class="ui-input" placeholder="如: 活动赠送">
</div>
</div>
<div class="modal-footer">
<button class="ui-btn ui-btn-secondary" onclick="closeModal('generateModal')">取消</button>
<button class="ui-btn ui-btn-primary" onclick="generateCodes()">生成</button>
</div>
</div>
</div>
<!-- 生成结果弹窗 -->
<div class="modal" id="resultModal">
<div class="modal-content">
<div class="modal-header">
<h3>生成成功</h3>
<button class="modal-close" onclick="closeModal('resultModal')">&times;</button>
</div>
<div class="modal-body">
<p>批次ID: <strong id="resultBatchId"></strong></p>
<p>生成数量: <strong id="resultCount"></strong></p>
<div class="form-group">
<label>兑换码列表</label>
<textarea id="resultCodes" class="ui-input" rows="10" readonly style="font-family: monospace;"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="ui-btn ui-btn-secondary" onclick="closeModal('resultModal')">关闭</button>
<button class="ui-btn ui-btn-primary" onclick="copyResultCodes()">复制全部</button>
</div>
</div>
</div>
<style>
.ui-select {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: white;
min-width: 150px;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 500;
}
.status-badge.unused {
background: #dcfce7;
color: #166534;
}
.status-badge.used {
background: #e0e7ff;
color: #3730a3;
}
.status-badge.expired {
background: #fee2e2;
color: #991b1b;
}
.code-text {
font-family: monospace;
font-size: 0.875rem;
background: var(--secondary-100);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
margin: 0;
font-size: 1.125rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-muted);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--secondary-700);
}
.form-group small {
display: block;
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.75rem;
}
.required {
color: #dc2626;
}
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.page-btn {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
background: white;
border-radius: var(--radius-sm);
cursor: pointer;
}
.page-btn.active {
background: var(--primary-600);
color: white;
border-color: var(--primary-600);
}
</style>
<script src="/static/js/ui-components.js"></script>
<script>
let currentPage = 1;
let userGroups = [];
async function logout() {
try {
await fetch('/admin/logout', { method: 'POST' });
window.location.href = '/admin/login';
} catch (e) {
UI.notify('退出失败', 'error');
}
}
async function loadUserGroups() {
try {
const response = await fetch('/admin/api/user-groups');
const result = await response.json();
if (result.success) {
userGroups = result.data;
const select = document.getElementById('targetGroup');
select.innerHTML = '<option value="">请选择</option>' +
userGroups.map(g => `<option value="${g.id}">${g.name}</option>`).join('');
}
} catch (e) {
console.error('加载用户组失败', e);
}
}
async function loadBatches() {
try {
const response = await fetch('/admin/api/redeem-codes/batches');
const result = await response.json();
if (result.success) {
const select = document.getElementById('filterBatch');
select.innerHTML = '<option value="">全部批次</option>' +
result.data.map(b => `<option value="${b.batch_id}">${b.batch_id.slice(-12)} (${b.unused}/${b.total})</option>`).join('');
let total = 0, used = 0, unused = 0;
result.data.forEach(b => {
total += b.total;
used += b.used;
unused += b.unused;
});
document.getElementById('totalCount').textContent = total;
document.getElementById('usedCount').textContent = used;
document.getElementById('unusedCount').textContent = unused;
}
} catch (e) {
console.error('加载批次失败', e);
}
}
async function loadCodes() {
const status = document.getElementById('filterStatus').value;
const batchId = document.getElementById('filterBatch').value;
let url = `/admin/api/redeem-codes?page=${currentPage}`;
if (status) url += `&status=${status}`;
if (batchId) url += `&batch_id=${batchId}`;
try {
const response = await fetch(url);
const result = await response.json();
if (result.success) {
renderCodes(result.data);
renderPagination(result.pagination);
}
} catch (e) {
console.error('加载兑换码失败', e);
}
}
function renderCodes(codes) {
const tbody = document.getElementById('codesBody');
if (codes.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">暂无数据</td></tr>';
return;
}
tbody.innerHTML = codes.map(code => {
let statusClass = 'unused';
let statusText = '未使用';
if (code.is_used) {
statusClass = 'used';
statusText = '已使用';
} else if (code.is_expired) {
statusClass = 'expired';
statusText = '已过期';
}
return `
<tr>
<td><span class="code-text">${code.code}</span></td>
<td>${code.target_group}</td>
<td>${code.duration_days}天</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${code.used_by || '-'}</td>
<td>${code.used_at || '-'}</td>
<td>${code.expires_at || '永久'}</td>
<td>
${!code.is_used ? `<button class="ui-btn ui-btn-sm ui-btn-danger" onclick="deleteCode(${code.id})">删除</button>` : '-'}
</td>
</tr>
`;
}).join('');
}
function renderPagination(pagination) {
const container = document.getElementById('pagination');
if (pagination.pages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= pagination.pages; i++) {
html += `<button class="page-btn ${i === pagination.page ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
container.innerHTML = html;
}
function goToPage(page) {
currentPage = page;
loadCodes();
}
function showGenerateModal() {
document.getElementById('generateModal').classList.add('show');
}
function closeModal(id) {
document.getElementById(id).classList.remove('show');
}
async function generateCodes() {
const targetGroupId = document.getElementById('targetGroup').value;
const durationDays = document.getElementById('durationDays').value;
const count = document.getElementById('generateCount').value;
const prefix = document.getElementById('codePrefix').value;
const expiresDays = document.getElementById('expiresDays').value;
const remark = document.getElementById('remark').value;
if (!targetGroupId) {
UI.notify('请选择目标套餐', 'warning');
return;
}
try {
const response = await fetch('/admin/api/redeem-codes/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
target_group_id: parseInt(targetGroupId),
duration_days: parseInt(durationDays),
count: parseInt(count),
prefix: prefix,
expires_days: expiresDays ? parseInt(expiresDays) : null,
remark: remark
})
});
const result = await response.json();
if (result.success) {
closeModal('generateModal');
document.getElementById('resultBatchId').textContent = result.data.batch_id;
document.getElementById('resultCount').textContent = result.data.codes.length;
document.getElementById('resultCodes').value = result.data.codes.join('\n');
document.getElementById('resultModal').classList.add('show');
loadBatches();
loadCodes();
} else {
UI.notify(result.message, 'error');
}
} catch (e) {
UI.notify('生成失败', 'error');
}
}
function copyResultCodes() {
const textarea = document.getElementById('resultCodes');
textarea.select();
document.execCommand('copy');
UI.notify('已复制到剪贴板', 'success');
}
async function deleteCode(id) {
if (!confirm('确定要删除这个兑换码吗?')) return;
try {
const response = await fetch(`/admin/api/redeem-codes/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
UI.notify('删除成功', 'success');
loadBatches();
loadCodes();
} else {
UI.notify(result.message, 'error');
}
} catch (e) {
UI.notify('删除失败', 'error');
}
}
// 初始化
loadUserGroups();
loadBatches();
loadCodes();
</script>
</body>
</html>