341 lines
18 KiB
HTML
341 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>注册码管理</title>
|
||
<style>
|
||
body { margin:0; font-family: system-ui,-apple-system, Segoe UI, Roboto, sans-serif; background:#fafafa; }
|
||
.sidebar { position:fixed; top:0; left:0; width:180px; height:100vh; background:#1e1e2e; color:#fff; padding:20px; box-shadow:2px 0 5px rgba(0,0,0,0.1); z-index:1000; display:flex; flex-direction:column; align-items:center; }
|
||
.sidebar h3 { margin:0; font-size:18px; color:#add8e6; text-align:center; margin-bottom: 20px; }
|
||
.navigation-links { width:100%; margin-top:60px; }
|
||
.sidebar a, .sidebar button { display:block; color:#8be9fd; text-decoration:none; margin:10px 0; font-size:16px; padding:15px; border-radius:4px; background:transparent; border:none; cursor:pointer; width:calc(100% - 40px); text-align:left; transition:all .2s ease; }
|
||
.sidebar a:hover, .sidebar button:hover { color:#ff79c6; background-color:rgba(139,233,253,.2); }
|
||
.main { margin-left:200px; padding:20px; color:#333; }
|
||
.card { background:#fff; border-radius:14px; box-shadow:0 10px 24px rgba(31,35,40,.08); padding:20px; margin-bottom:20px; }
|
||
.row { display:flex; gap:16px; }
|
||
.col { flex:1; }
|
||
label { display:block; margin-bottom:6px; font-weight:600; }
|
||
input[type=text], input[type=number], select { width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:6px; box-sizing:border-box; }
|
||
.btn { padding:8px 12px; border:none; border-radius:8px; cursor:pointer; margin:0 4px; }
|
||
.btn-primary { background:#4f46e5; color:#fff; }
|
||
.btn-secondary { background:#64748b; color:#fff; }
|
||
.notice { padding:10px; border-radius:6px; margin-top:10px; display:none; }
|
||
.notice.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
|
||
.notice.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
|
||
.code-box { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding:12px; border:1px solid #e5e7eb; border-radius:8px; background:#fafafa; margin-top:10px; }
|
||
.overlay { position:fixed; inset:0; background:rgba(0,0,0,0.25); display:flex; align-items:center; justify-content:center; z-index:2000; }
|
||
.spinner { width:42px; height:42px; border:4px solid #cbd5e1; border-top-color:#4f46e5; border-radius:50%; animation:spin 0.8s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.fade-in { animation: fadeUp 0.25s ease-out; }
|
||
@keyframes fadeUp { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform: translateY(0); } }
|
||
table tr:hover { background-color:#f3f4f6; transition: background-color 0.2s ease; }
|
||
.btn { transition: transform 0.1s ease, box-shadow 0.2s ease; }
|
||
.btn:hover { transform: translateY(-1px); box-shadow:0 6px 16px rgba(31,35,40,0.12); }
|
||
</style>
|
||
{% csrf_token %}
|
||
<script>
|
||
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||
async function loadKeys(){
|
||
const resp=await fetch('/elastic/registration-codes/keys/');
|
||
const data=await resp.json();
|
||
const opts=(data.data||[]);
|
||
const keySel=document.getElementById('keys');
|
||
const mkeySel=document.getElementById('manageKeys');
|
||
keySel.innerHTML=''; mkeySel.innerHTML='';
|
||
opts.forEach(k=>{
|
||
const o=document.createElement('option'); o.value=k; o.textContent=k; keySel.appendChild(o);
|
||
const o2=document.createElement('option'); o2.value=k; o2.textContent=k; mkeySel.appendChild(o2);
|
||
});
|
||
}
|
||
async function addKey(){
|
||
const keyName=(document.getElementById('newKey').value||'').trim();
|
||
if(!keyName) return;
|
||
const csrftoken=getCookie('csrftoken');
|
||
const resp=await fetch('/elastic/registration-codes/keys/add/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({key:keyName})});
|
||
const data=await resp.json();
|
||
const msg=document.getElementById('msg');
|
||
if(resp.ok && data.status==='success'){msg.textContent='新增key成功'; msg.className='notice success'; msg.style.display='block'; document.getElementById('newKey').value=''; loadKeys();}
|
||
else{msg.textContent=data.message||'新增失败'; msg.className='notice error'; msg.style.display='block';}
|
||
}
|
||
function selectedValues(sel){return Array.from(sel.selectedOptions).map(o=>o.value);}
|
||
function enableToggleSelect(sel){ sel.addEventListener('mousedown',function(e){ if(e.target && e.target.tagName==='OPTION'){ e.preventDefault(); const op=e.target; op.selected=!op.selected; this.dispatchEvent(new Event('change',{bubbles:true})); } }); }
|
||
function clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
|
||
async function generateCode(){
|
||
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||
const csrftoken=getCookie('csrftoken');
|
||
const keys=selectedValues(document.getElementById('keys'));
|
||
const manageKeys=selectedValues(document.getElementById('manageKeys'));
|
||
const mode=document.getElementById('expireMode').value;
|
||
let days=30; if(mode==='month') days=30; else if(mode==='fouryears') days=1460; else { const d=parseInt(document.getElementById('customDays').value||'30'); days=isNaN(d)?30:Math.max(1,d);}
|
||
const resp=await fetch('/elastic/registration-codes/generate/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({keys,manage_keys:manageKeys,expires_in_days:days})});
|
||
const data=await resp.json();
|
||
const out=document.getElementById('codeOut');
|
||
const msg=document.getElementById('msg');
|
||
if(resp.ok && data.status==='success'){out.textContent=data.data.code; msg.textContent='生成成功'; msg.className='notice success'; msg.style.display='block';}
|
||
else{msg.textContent=data.message||'生成失败'; msg.className='notice error'; msg.style.display='block';}
|
||
ov.style.display='none';
|
||
}
|
||
async function loadCodes(){
|
||
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||
const resp=await fetch('/elastic/registration-codes/list/');
|
||
const data=await resp.json();
|
||
const tbody=document.getElementById('codesBody');
|
||
if(!tbody) return;
|
||
tbody.innerHTML='';
|
||
if(resp.ok && data.status==='success'){
|
||
(data.data||[]).forEach(it=>{
|
||
const tr=document.createElement('tr');
|
||
const status = it.active? '有效' : '失效';
|
||
const ka = Array.isArray(it.keys)? it.keys.join('、') : '';
|
||
const mka = Array.isArray(it.manage_keys)? it.manage_keys.join('、') : '';
|
||
tr.innerHTML = `<td>${it.code||''}</td><td>${ka}</td><td>${mka}</td><td>${formatDate(it.created_at)}</td><td>${formatDate(it.expires_at)}</td><td>${status}</td><td>${it.active? '<button class=\"btn btn-secondary\" data-code=\"'+it.code+'\">作废</button>':''}</td>`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
ov.style.display='none';
|
||
}
|
||
function formatDate(t){ if(!t) return ''; try{ const d = new Date(t); if(String(d)!='Invalid Date'){ const p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;} }catch(e){} return ''; }
|
||
async function revokeCode(code){ const csrftoken=getCookie('csrftoken'); const resp=await fetch('/elastic/registration-codes/revoke/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({code})}); const msg=document.getElementById('msg'); const data=await resp.json(); if(resp.ok && data.status==='success'){ msg.textContent='已作废'; msg.className='notice success'; msg.style.display='block'; loadCodes(); } else { msg.textContent=data.message||'作废失败'; msg.className='notice error'; msg.style.display='block'; } }
|
||
document.addEventListener('click',function(e){ const btn=e.target; if(btn && btn.matches('button[data-code]')){ revokeCode(btn.getAttribute('data-code')); }});
|
||
document.addEventListener('DOMContentLoaded',()=>{loadKeys(); enableToggleSelect(document.getElementById('keys')); enableToggleSelect(document.getElementById('manageKeys')); loadCodes();});
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<div id="overlay" class="overlay" style="display:none"><div class="spinner"></div></div>
|
||
<div class="sidebar">
|
||
<h3>你好,{{ username|default:"访客" }}</h3>
|
||
<div class="navigation-links">
|
||
<a href="{% url 'main:home' %}">返回主页</a>
|
||
<a id="logoutBtn">退出登录</a>
|
||
<div id="logoutMsg"></div>
|
||
{% csrf_token %}
|
||
</div>
|
||
</div>
|
||
<div class="main">
|
||
<div class="card fade-in">
|
||
<h2>管理注册码</h2>
|
||
<div class="row">
|
||
<div class="col">
|
||
<label>新增key</label>
|
||
<input id="newKey" type="text" placeholder="输入新的key" />
|
||
<button class="btn btn-secondary" onclick="addKey()">新增</button>
|
||
</div>
|
||
</div>
|
||
<div class="row" style="margin-top:12px;">
|
||
<div class="col">
|
||
<label>选择keys</label>
|
||
<select id="keys" multiple size="10"></select>
|
||
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('keys')">清空选择</button></div>
|
||
</div>
|
||
<div class="col">
|
||
<label>选择manage_keys</label>
|
||
<select id="manageKeys" multiple size="10"></select>
|
||
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('manageKeys')">清空选择</button></div>
|
||
</div>
|
||
</div>
|
||
<div class="row" style="margin-top:12px;">
|
||
<div class="col">
|
||
<label>有效期</label>
|
||
<select id="expireMode">
|
||
<option value="month">一个月</option>
|
||
<option value="fouryears">四年</option>
|
||
<option value="custom">自定义天数</option>
|
||
</select>
|
||
<input id="customDays" type="number" min="1" placeholder="自定义天数" />
|
||
</div>
|
||
<div class="col" style="display:flex; align-items:flex-end;">
|
||
<button class="btn btn-primary" onclick="generateCode()">生成注册码</button>
|
||
</div>
|
||
</div>
|
||
<div id="msg" class="notice"></div>
|
||
<div class="code-box" id="codeOut"></div>
|
||
<div class="row" style="margin-top:12px;">
|
||
<div class="col">
|
||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||
<h3>已生成的注册码</h3>
|
||
<div>
|
||
<button class="btn btn-secondary" onclick="loadCodes()">刷新列表</button>
|
||
</div>
|
||
</div>
|
||
<table style="width:100%; border-collapse:collapse; margin-top:10px;">
|
||
<thead>
|
||
<tr>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">code</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">keys</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">manage_keys</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">创建时间</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">过期时间</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">状态</th>
|
||
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="codesBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
// 获取CSRF令牌的函数
|
||
function getCookie(name) {
|
||
const value = `; ${document.cookie}`;
|
||
const parts = value.split(`; ${name}=`);
|
||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||
}
|
||
|
||
// 导航点击处理函数,提供备用URL
|
||
function handleNavClick(element, fallbackUrl) {
|
||
// 尝试使用Django模板生成的URL,如果失败则使用备用URL
|
||
try {
|
||
// 如果模板渲染正常,直接返回true让默认行为处理
|
||
return true;
|
||
} catch (e) {
|
||
// 如果模板渲染有问题,使用备用URL
|
||
window.location.href = fallbackUrl;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 修复用户管理链接跳转问题
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 为用户管理链接添加事件监听器,确保正确跳转
|
||
const userManagementLink = document.querySelector('a[href*="get_users"]');
|
||
if (userManagementLink) {
|
||
userManagementLink.addEventListener('click', function(e) {
|
||
// 阻止默认行为
|
||
e.preventDefault();
|
||
|
||
// 获取备用URL
|
||
const fallbackUrl = this.getAttribute('onclick').match(/'([^']+)'/g)[1].replace(/'/g, '');
|
||
|
||
// 直接跳转到用户管理页面
|
||
window.location.href = fallbackUrl;
|
||
});
|
||
}
|
||
});
|
||
|
||
// 登出功能
|
||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||
const msg = document.getElementById('logoutMsg');
|
||
msg.textContent = '';
|
||
const csrftoken = getCookie('csrftoken');
|
||
try {
|
||
const resp = await fetch('/accounts/logout/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken || ''
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.ok) {
|
||
throw new Error('登出失败');
|
||
}
|
||
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
document.cookie = 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
window.location.href = data.redirect_url;
|
||
} catch (e) {
|
||
msg.textContent = e.message || '发生错误';
|
||
}
|
||
});
|
||
|
||
|
||
function fetchJSON(url){ return fetch(url, {credentials:'same-origin'}).then(r=>r.json()); }
|
||
function qs(params){ const u = new URLSearchParams(params); return u.toString(); }
|
||
|
||
const trendChart = echarts.init(document.getElementById('chartTrend'));
|
||
const typesChart = echarts.init(document.getElementById('chartTypes'));
|
||
const typesTrendChart = echarts.init(document.getElementById('chartTypesTrend'));
|
||
|
||
async function loadTrend(){
|
||
const url = '/elastic/analytics/trend/?' + qs({ from:'now-90d', to:'now', interval:'day' });
|
||
const res = await fetchJSON(url);
|
||
if(res.status!=='success') return;
|
||
const buckets = res.data || [];
|
||
const x = buckets.map(b=>b.key_as_string||'');
|
||
const y = buckets.map(b=>b.doc_count||0);
|
||
trendChart.setOption({
|
||
tooltip:{trigger:'axis'},
|
||
xAxis:{type:'category', data:x},
|
||
yAxis:{type:'value'},
|
||
series:[{ type:'line', areaStyle:{}, data:y, smooth:true, color:'#4f46e5' }]
|
||
});
|
||
}
|
||
|
||
async function loadTypes(){
|
||
const url = '/elastic/analytics/types/?' + qs({ from:'now-30d', to:'now', size:10 });
|
||
const res = await fetchJSON(url);
|
||
if(res.status!=='success') return;
|
||
const buckets = res.data || [];
|
||
const data = buckets.map(b=>({ name: String(b.key||'未知'), value: b.doc_count||0 }));
|
||
typesChart.setOption({
|
||
tooltip:{trigger:'item'},
|
||
legend:{type:'scroll'},
|
||
series:[{ type:'pie', radius:['40%','70%'], data }]
|
||
});
|
||
}
|
||
|
||
async function loadTypesTrend(){
|
||
const url = '/elastic/analytics/types_trend/?' + qs({ from:'now-180d', to:'now', interval:'week', size:6 });
|
||
const res = await fetchJSON(url);
|
||
if(res.status!=='success') return;
|
||
const rows = res.data || [];
|
||
const x = rows.map(r=>r.key_as_string||'');
|
||
const typeSet = new Set();
|
||
rows.forEach(r=> (r.types||[]).forEach(t=> typeSet.add(String(t.key||'未知'))));
|
||
const types = Array.from(typeSet);
|
||
const series = types.map(tp=>({
|
||
name: tp,
|
||
type:'line',
|
||
smooth:true,
|
||
data: rows.map(r=>{
|
||
const b = (r.types||[]).find(x=>String(x.key||'')===tp);
|
||
return b? b.doc_count||0 : 0;
|
||
})
|
||
}));
|
||
typesTrendChart.setOption({
|
||
tooltip:{trigger:'axis'},
|
||
legend:{type:'scroll'},
|
||
xAxis:{type:'category', data:x},
|
||
yAxis:{type:'value'},
|
||
series
|
||
});
|
||
}
|
||
|
||
function formatTime(t){
|
||
try{
|
||
const d = new Date(t);
|
||
if(String(d) !== 'Invalid Date'){
|
||
const pad = n=> String(n).padStart(2,'0');
|
||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||
}
|
||
}catch(e){}
|
||
return t||'';
|
||
}
|
||
|
||
async function loadRecent(){
|
||
const listEl = document.getElementById('recentList');
|
||
const url = '/elastic/analytics/recent/?' + qs({ from:'now-7d', to:'now', limit:10 });
|
||
const res = await fetchJSON(url);
|
||
if(res.status!=='success') return;
|
||
const items = res.data || [];
|
||
listEl.innerHTML = '';
|
||
items.forEach(it=>{
|
||
const li = document.createElement('li');
|
||
const t = formatTime(it.time);
|
||
const u = it.username || '';
|
||
const ty = it.type || '未知';
|
||
li.textContent = `${t},${u},${ty}`;
|
||
listEl.appendChild(li);
|
||
});
|
||
}
|
||
|
||
loadTrend();
|
||
loadTypes();
|
||
loadTypesTrend();
|
||
loadRecent();
|
||
</script>
|
||
</body>
|
||
</html> |