v0.3.0: 管理后台UI全面美化
- 全新玻璃拟态设计(Glassmorphism) - 渐变背景+模糊效果 - 统计面板集成(今日搜索/转存/总计) - 功能卡片悬停动画+状态指示条 - 滑块开关带发光效果 - Toast通知动画优化 - 完全响应式适配移动端
This commit is contained in:
@@ -3,147 +3,102 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CloudSearch 管理后台 v2.2</title>
|
||||
<title>CloudSearch 管理后台</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #111827; --card: #1f2937; --border: #374151;
|
||||
--text: #f3f4f6; --muted: #9ca3af; --accent: #3b82f6;
|
||||
--accent-hover: #2563eb; --on: #22c55e; --off: #ef4444;
|
||||
--danger: #dc2626;
|
||||
--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, '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; }
|
||||
*{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>
|
||||
<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>
|
||||
|
||||
<header class="header"><div class="header-inner"><div class="logo"><div class="logo-icon">⚙</div><div class="logo-text"><h1>CloudSearch 管理后台</h1><span>功能开关 & 系统配置</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>
|
||||
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();
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user