v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
149
cloudsearch_admin/templates/admin.html
Normal file
149
cloudsearch_admin/templates/admin.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CloudSearch 管理后台 v2.2</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #111827; --card: #1f2937; --border: #374151;
|
||||
--text: #f3f4f6; --muted: #9ca3af; --accent: #3b82f6;
|
||||
--accent-hover: #2563eb; --on: #22c55e; --off: #ef4444;
|
||||
--danger: #dc2626;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
||||
header { background: var(--card); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
|
||||
header h1 { font-size: 20px; font-weight: 700; }
|
||||
header .meta { font-size: 12px; color: var(--muted); }
|
||||
.toolbar { display: flex; gap: 8px; }
|
||||
.btn { padding: 8px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--text); cursor: pointer; font-size: 13px; transition: .15s; }
|
||||
.btn:hover { background: #374151; }
|
||||
.btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
|
||||
.btn.primary:hover { background: var(--accent-hover); }
|
||||
.btn.danger { background: var(--danger); border-color: var(--danger); color: white; }
|
||||
main { max-width: 720px; margin: 24px auto; padding: 0 16px; }
|
||||
.group-title { font-size: 13px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); padding: 12px 0 8px; font-weight: 600; }
|
||||
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; margin-bottom: 12px; }
|
||||
.feature-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.feature-info h3 { font-size: 15px; margin-bottom: 2px; }
|
||||
.feature-info .code { font-size: 11px; color: var(--muted); font-family: monospace; background: var(--bg); padding: 1px 6px; border-radius: 4px; }
|
||||
.toggle { width: 52px; height: 28px; border-radius: 28px; border: none; cursor: pointer; position: relative; transition: background .2s; }
|
||||
.toggle.off { background: #4b5563; }
|
||||
.toggle.on { background: var(--on); }
|
||||
.toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: white; transition: transform .2s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
|
||||
.toggle.on::after { transform: translateX(24px); }
|
||||
.sync-badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; margin-left: 8px; }
|
||||
.sync-badge.ok { background: rgba(34,197,94,.2); color: var(--on); }
|
||||
.sync-badge.mismatch { background: rgba(239,68,68,.2); color: var(--off); }
|
||||
.toast { position: fixed; bottom: 24px; right: 24px; background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 12px 20px; font-size: 14px; opacity: 0; transform: translateY(20px); transition: .25s; z-index: 100; }
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
.toast.ok { border-color: var(--on); }
|
||||
.toast.err { border-color: var(--off); }
|
||||
.loading { opacity: .5; pointer-events: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1>🔧 CloudSearch 管理后台</h1>
|
||||
<div class="meta">v2.2 · 功能开关一键管理</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="location.reload()">🔄 刷新</button>
|
||||
<button class="btn primary" onclick="saveAll()">💾 全部保存</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<div style="text-align:center;padding:60px;color:var(--muted)">加载中...</div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
const FEATURES = {{ features | tojson }};
|
||||
|
||||
function groupBy(list, key) {
|
||||
return list.reduce((acc, [k,v]) => {
|
||||
(acc[v[key]] = acc[v[key]] || []).push([k,v]);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const res = await fetch('/api/flags');
|
||||
const data = await res.json();
|
||||
const entries = Object.entries(FEATURES).map(([k, meta]) => [k, { ...meta, flag: data[k] }]);
|
||||
const groups = groupBy(entries, 'group');
|
||||
|
||||
let html = '';
|
||||
const order = ['核心', '增强', '转存'];
|
||||
for (const g of order) {
|
||||
if (!groups[g]) continue;
|
||||
html += `<div class="group-title">${g}功能</div>`;
|
||||
for (const [key, item] of groups[g]) {
|
||||
const flag = item.flag || {};
|
||||
const on = flag.value;
|
||||
const synced = flag.synced !== false;
|
||||
const updated = flag.updated_at ? flag.updated_at.replace('T',' ').slice(0,16) : '';
|
||||
html += `
|
||||
<div class="card" id="card-${key}">
|
||||
<div class="feature-row">
|
||||
<div class="feature-info">
|
||||
<h3>
|
||||
${item.name}
|
||||
${!synced ? '<span class="sync-badge mismatch">⚠ 未同步</span>' : ''}
|
||||
</h3>
|
||||
<span class="code">${key}</span>
|
||||
${updated ? `<span style="font-size:11px;color:var(--muted);margin-left:6px">${updated}</span>` : ''}
|
||||
</div>
|
||||
<button class="toggle ${on ? 'on' : 'off'}" onclick="toggle('${key}', ${!on})" title="点击切换"><span style="display:none">开关</span></button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
document.getElementById('main').innerHTML = html;
|
||||
}
|
||||
|
||||
async function toggle(key, value) {
|
||||
const card = document.getElementById('card-' + key);
|
||||
card.classList.add('loading');
|
||||
try {
|
||||
const res = await fetch(`/api/flags/${key}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({value})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showToast(`${FEATURES[key].name} → ${value ? '✅ 已开启' : '❌ 已关闭'}`, 'ok');
|
||||
load();
|
||||
} else {
|
||||
showToast(data.error || '未知错误', 'err');
|
||||
}
|
||||
} catch(e) {
|
||||
showToast('网络错误: ' + e.message, 'err');
|
||||
}
|
||||
card.classList.remove('loading');
|
||||
}
|
||||
|
||||
async function saveAll() {
|
||||
const res = await fetch('/api/flags');
|
||||
const data = await res.json();
|
||||
showToast('所有开关状态已保存 ✓', 'ok');
|
||||
}
|
||||
|
||||
function showToast(msg, type) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.className = 'toast ' + (type || 'ok') + ' show';
|
||||
clearTimeout(el._t);
|
||||
el._t = setTimeout(() => el.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user