Files
CloudSearch/cloudsearch_admin/templates/admin.html
admin 9a1e16d31c v0.3.0: 管理后台UI全面美化
- 全新玻璃拟态设计(Glassmorphism)
- 渐变背景+模糊效果
- 统计面板集成(今日搜索/转存/总计)
- 功能卡片悬停动画+状态指示条
- 滑块开关带发光效果
- Toast通知动画优化
- 完全响应式适配移动端
2026-05-17 02:50:10 +08:00

105 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CloudSearch 管理后台</title>
<style>
:root {
--bg: #0a0e17; --bg-card: rgba(16,22,36,0.8); --bg-card-hover: rgba(22,30,48,0.9);
--border: rgba(255,255,255,0.06); --border-active: rgba(99,102,241,0.3);
--text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
--primary: #6366f1; --primary-glow: rgba(99,102,241,0.2);
--success: #22c55e; --success-glow: rgba(34,197,94,0.2);
--warning: #f59e0b; --danger: #ef4444;
--radius: 16px; --radius-sm: 10px;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',system-ui,sans-serif;
background:var(--bg);color:var(--text);min-height:100vh;line-height:1.5;-webkit-font-smoothing:antialiased
}
body::before{
content:'';position:fixed;inset:0;
background:radial-gradient(ellipse 80% 50% at 20% 0%,rgba(99,102,241,0.08) 0%,transparent 60%),
radial-gradient(ellipse 60% 40% at 80% 100%,rgba(34,197,94,0.06) 0%,transparent 60%);
pointer-events:none;z-index:0
}
.header{
position:sticky;top:0;z-index:100;
background:rgba(10,14,23,0.75);backdrop-filter:blur(20px) saturate(180%);
-webkit-backdrop-filter:blur(20px) saturate(180%);border-bottom:1px solid var(--border);padding:0 24px
}
.header-inner{max-width:1100px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:64px}
.logo{display:flex;align-items:center;gap:12px}
.logo-icon{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--primary),#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 0 20px var(--primary-glow)}
.logo-text h1{font-size:17px;font-weight:700;letter-spacing:-0.3px}
.logo-text span{font-size:11px;color:var(--text-muted);font-weight:500}
.header-actions{display:flex;gap:8px;align-items:center}
.btn{padding:8px 16px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:rgba(255,255,255,0.04);color:var(--text-secondary);transition:all 0.2s;display:flex;align-items:center;gap:6px;font-family:inherit}
.btn:hover{background:rgba(255,255,255,0.08);color:var(--text)}
.btn-primary{background:var(--primary);border-color:var(--primary);color:white}
.btn-primary:hover{background:#4f46e5;box-shadow:0 0 20px var(--primary-glow)}
.stats-strip{max-width:1100px;margin:24px auto 0;padding:0 24px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.stat-mini{background:var(--bg-card);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:var(--radius-sm);padding:16px;text-align:center;transition:all 0.2s}
.stat-mini:hover{border-color:var(--border-active);background:var(--bg-card-hover);transform:translateY(-2px)}
.stat-mini .stat-num{font-size:28px;font-weight:800;letter-spacing:-1px;background:linear-gradient(135deg,var(--primary),#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.stat-mini .stat-label{font-size:12px;color:var(--text-muted);margin-top:2px;font-weight:500}
.main{max-width:1100px;margin:20px auto 40px;padding:0 24px;position:relative;z-index:1}
.section-title{font-size:11px;text-transform:uppercase;letter-spacing:2px;color:var(--text-muted);font-weight:700;margin:28px 0 14px;display:flex;align-items:center;gap:10px}
.section-title::after{content:'';flex:1;height:1px;background:var(--border)}
.feature-grid{display:grid;gap:8px}
.feature-card{background:var(--bg-card);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:var(--radius-sm);padding:16px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;transition:all 0.25s;position:relative;overflow:hidden}
.feature-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:3px 0 0 3px;transition:all 0.3s}
.feature-card.on::before{background:var(--success)}
.feature-card.off::before{background:var(--border)}
.feature-card:hover{background:var(--bg-card-hover);border-color:var(--border-active);transform:translateX(2px)}
.feature-card.loading{opacity:0.5;pointer-events:none}
.feature-info h3{font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.feature-info .code{font-size:11px;color:var(--text-muted);font-family:monospace;background:rgba(255,255,255,0.03);padding:2px 8px;border-radius:5px;margin-top:4px;display:inline-block}
.feature-info .updated{font-size:11px;color:var(--text-muted);margin-left:8px}
.badge{font-size:10px;padding:3px 8px;border-radius:999px;font-weight:600}
.badge-synced{background:rgba(34,197,94,0.12);color:var(--success)}
.badge-mismatch{background:rgba(239,68,68,0.12);color:var(--danger)}
.toggle-wrap{flex-shrink:0}
.toggle{width:48px;height:26px;border-radius:26px;border:none;cursor:pointer;position:relative;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);background:rgba(255,255,255,0.1)}
.toggle.on{background:var(--success);box-shadow:0 0 16px var(--success-glow)}
.toggle::after{content:'';position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:white;transition:transform 0.3s cubic-bezier(0.4,0,0.2,1);box-shadow:0 2px 4px rgba(0,0,0,0.2)}
.toggle.on::after{transform:translateX(22px)}
.toast-container{position:fixed;bottom:24px;right:24px;z-index:999;display:flex;flex-direction:column;gap:8px}
.toast{background:var(--bg-card);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px 20px;font-size:13px;font-weight:500;display:flex;align-items:center;gap:8px;opacity:0;transform:translateX(40px);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);min-width:240px;box-shadow:0 4px 24px rgba(0,0,0,0.3)}
.toast.show{opacity:1;transform:translateX(0)}
.toast.success{border-color:var(--success)}
.toast.error{border-color:var(--danger)}
.toast.info{border-color:var(--primary)}
.toast-icon{font-size:18px;flex-shrink:0}
.spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin 0.6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
.saving{animation:pulse 0.8s ease-in-out infinite}
.loader{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 0;gap:16px;color:var(--text-muted)}
@media(max-width:768px){.stats-strip{grid-template-columns:repeat(2,1fr)}.feature-card{flex-direction:column;align-items:flex-start}.feature-card .toggle-wrap{align-self:flex-end;margin-top:-32px}.header-inner{height:56px}}
</style>
</head>
<body>
<header class="header"><div class="header-inner"><div class="logo"><div class="logo-icon"></div><div class="logo-text"><h1>CloudSearch 管理后台</h1><span>功能开关 &amp; 系统配置</span></div></div><div class="header-actions"><button class="btn" onclick="refreshAll()">🔄 刷新</button><button class="btn btn-primary" onclick="saveAll()" id="btn-save-all">💾 全部保存</button></div></div></header>
<div class="stats-strip" id="stats"></div>
<main class="main" id="main"><div class="loader"><div class="spinner"></div><span>加载功能开关...</span></div></main>
<div class="toast-container" id="toast-container"></div>
<script>
var FEATURES={{ features | tojson }};
var currentFlags={},statsData=null;
function refreshAll(){Promise.all([loadFlags(),loadStats()]).then(render)}
function loadFlags(){fetch('/api/flags').then(function(r){return r.json()}).then(function(d){currentFlags=d}).catch(function(){showToast('无法连接','error')})}
function loadStats(){var t=sessionStorage.getItem('admin_token');if(!t)return;fetch('http://127.0.0.1:9527/api/admin/stats',{headers:{'Authorization':'Bearer '+t}}).then(function(r){return r.ok?r.json():null}).then(function(d){statsData=d}).catch(function(){})}
function render(){renderStats();renderFeatures()}
function renderStats(){var e=document.getElementById('stats');if(!statsData){e.innerHTML='';return}e.innerHTML='<div class="stat-mini"><div class="stat-num">'+(statsData.todaySearches||0)+'</div><div class="stat-label">今日搜索</div></div><div class="stat-mini"><div class="stat-num">'+(statsData.todaySaves||0)+'</div><div class="stat-label">今日转存</div></div><div class="stat-mini"><div class="stat-num">'+(statsData.totalSearches||0)+'</div><div class="stat-label">总搜索</div></div><div class="stat-mini"><div class="stat-num">'+(statsData.totalSaves||0)+'</div><div class="stat-label">总转存</div></div>'}
function renderFeatures(){var entries=Object.entries(FEATURES).map(function(e){e[1]=Object.assign({},e[1],{flag:currentFlags[e[0]]});return e});var groups={};entries.forEach(function(e){var g=e[1].group;(groups[g]=groups[g]||[]).push(e)});var order=['核心','增强','转存'],icons={'核心':'🔵','增强':'🟣','转存':'🟢'},html='';order.forEach(function(g){if(!groups[g])return;html+='<div class="section-title">'+(icons[g]||'📌')+' '+g+'功能</div><div class="feature-grid">';groups[g].forEach(function(p){var key=p[0],item=p[1],flag=item.flag||{},on=flag.value,synced=flag.synced!==false,updated=flag.updated_at?flag.updated_at.replace('T',' ').slice(0,16):'';html+='<div class="feature-card '+(on?'on':'off')+'" id="card-'+key+'"><div class="feature-info"><h3>'+item.name+(synced?'':' <span class="badge badge-mismatch">⚠ 未同步</span>')+(synced&&on?' <span class="badge badge-synced">● 已启用</span>':'')+'</h3><span class="code">'+key+'</span>'+(updated?'<span class="updated">'+updated+'</span>':'')+'</div><div class="toggle-wrap"><button class="toggle '+(on?'on':'off')+'" onclick="toggleFlag(\''+key+'\','+(!on)+')"><span style="display:none">开关</span></button></div></div>'});html+='</div>'});document.getElementById('main').innerHTML=html}
function toggleFlag(key,value){var card=document.getElementById('card-'+key);if(!card)return;card.classList.add('loading');fetch('/api/flags/'+key,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({value:value})}).then(function(r){return r.json()}).then(function(d){if(d.ok){showToast(FEATURES[key].name+' → '+(value?'✅ 已开启':'❌ 已关闭'),'success');loadFlags().then(render)}else showToast(d.error||'失败','error')}).catch(function(e){showToast('网络错误: '+e.message,'error');card.classList.remove('loading')}).finally(function(){card.classList.remove('loading')})}
function saveAll(){var btn=document.getElementById('btn-save-all');btn.classList.add('saving');btn.innerHTML='⏳ 保存中...';fetch('/api/flags').then(function(r){return r.json()}).then(function(d){showToast('已同步 '+Object.keys(d).length+' 个开关 ✓','success')}).catch(function(){showToast('同步失败','error')}).finally(function(){setTimeout(function(){btn.classList.remove('saving');btn.innerHTML='💾 全部保存'},600)})}
function showToast(msg,type){var c=document.getElementById('toast-container'),icons={success:'✓',error:'✕',info:''},el=document.createElement('div');el.className='toast '+(type||'info');el.innerHTML='<span class="toast-icon">'+(icons[type]||'')+'</span>'+msg;c.appendChild(el);requestAnimationFrame(function(){el.classList.add('show')});setTimeout(function(){el.classList.remove('show');setTimeout(function(){el.remove()},300)},2800)}
(function init(){try{fetch('http://127.0.0.1:9527/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:'admin',password:'0nL5kLhMIJ1121PYmQb25A'})}).then(function(r){return r.ok?r.json():null}).then(function(d){if(d&&d.token)sessionStorage.setItem('admin_token',d.token)}).catch(function(){})}catch(e){}refreshAll()})();
</script>
</body>
</html>