mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 07:19:33 +08:00
405 lines
17 KiB
HTML
405 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>BingPaper - 必应每日一图</title>
|
||
<style>
|
||
:root {
|
||
--primary-color: #007bff;
|
||
--danger-color: #dc3545;
|
||
--bg-color: #f8f9fa;
|
||
--card-bg: #ffffff;
|
||
--text-color: #333;
|
||
}
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background: var(--bg-color); color: var(--text-color); line-height: 1.6; }
|
||
nav { background: #333; color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 1000; }
|
||
nav a { color: white; text-decoration: none; margin-left: 1.5rem; font-weight: 500; }
|
||
nav .logo { font-size: 1.5rem; font-weight: bold; margin-left: 0; }
|
||
|
||
.container { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
|
||
.hidden { display: none !important; }
|
||
|
||
/* Gallery Styles */
|
||
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
|
||
.gallery-item { position: relative; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); background: #eee; aspect-ratio: 16 / 9; transition: transform 0.3s ease; }
|
||
.gallery-item:hover { transform: translateY(-5px); }
|
||
.gallery-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||
.gallery-item .overlay {
|
||
position: absolute; bottom: 0; left: 0; right: 0;
|
||
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||
color: white; padding: 20px 15px 10px;
|
||
opacity: 0; transition: opacity 0.3s ease;
|
||
}
|
||
.gallery-item:hover .overlay { opacity: 1; }
|
||
.gallery-item .title { font-weight: bold; font-size: 1.1rem; margin-bottom: 5px; }
|
||
.gallery-item .copyright { font-size: 0.85rem; opacity: 0.9; }
|
||
.gallery-item .date { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; }
|
||
|
||
/* Info Section */
|
||
.info-card { background: var(--card-bg); padding: 2rem; border-radius: 12px; margin-bottom: 2rem; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
|
||
.info-card ul { list-style: none; padding: 0; }
|
||
.info-card li { margin-bottom: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.preview-link {
|
||
text-decoration: none;
|
||
color: var(--primary-color);
|
||
font-size: 0.85rem;
|
||
padding: 2px 8px;
|
||
border: 1px solid var(--primary-color);
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
}
|
||
.preview-link:hover {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
code { background: #f1f1f1; padding: 2px 5px; border-radius: 4px; color: #e83e8c; }
|
||
pre { background: #f1f1f1; padding: 1rem; border-radius: 8px; overflow-x: auto; }
|
||
|
||
/* Admin/Login Form Styles */
|
||
.form-card { max-width: 400px; margin: 5rem auto; background: white; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
|
||
.form-card h2 { margin-top: 0; text-align: center; }
|
||
input, select { width: 100%; padding: 12px; margin: 10px 0 20px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
|
||
button { width: 100%; padding: 12px; border: none; border-radius: 6px; background: var(--primary-color); color: white; font-size: 1rem; cursor: pointer; transition: background 0.2s; }
|
||
button:hover { background: #0069d9; }
|
||
button.danger { background: var(--danger-color); }
|
||
button.danger:hover { background: #c82333; }
|
||
button.secondary { background: #6c757d; margin-top: 10px; }
|
||
|
||
/* Admin Panel Styles */
|
||
.admin-grid { display: grid; grid-template-columns: 1fr; gap: 20px; }
|
||
@media (min-width: 768px) { .admin-grid { grid-template-columns: 2fr 1fr; } }
|
||
.admin-card { background: white; padding: 1.5rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 20px; }
|
||
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
||
.status-badge { padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; }
|
||
.status-enabled { background: #d4edda; color: #155724; }
|
||
.status-disabled { background: #f8d7da; color: #721c24; }
|
||
|
||
.loading { text-align: center; padding: 3rem; font-size: 1.2rem; color: #666; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav>
|
||
<a href="/" class="logo">BingPaper</a>
|
||
<div>
|
||
<a href="/">首页</a>
|
||
<a href="/admin">管理</a>
|
||
<span id="nav-logout-container" class="hidden">
|
||
<a href="#" onclick="logout()">退出</a>
|
||
</span>
|
||
</div>
|
||
</nav>
|
||
|
||
<div id="content-home" class="container hidden">
|
||
<div class="info-card">
|
||
<h2>使用说明</h2>
|
||
<p>这是一个自动抓取必应每日一图并提供多分辨率管理的工具。您可以直接通过以下接口获取图片:</p>
|
||
<ul>
|
||
<li><span>今日图片:</span><code>/api/v1/image/today</code> <a href="/api/v1/image/today" target="_blank" class="preview-link">立即预览</a></li>
|
||
<li><span>今日元数据:</span><code>/api/v1/image/today/meta</code> <a href="/api/v1/image/today/meta" target="_blank" class="preview-link">立即预览</a></li>
|
||
<li><span>随机图片:</span><code>/api/v1/image/random</code> <a href="/api/v1/image/random" target="_blank" class="preview-link">立即预览</a></li>
|
||
<li><span>指定日期:</span><code>/api/v1/image/date/2026-01-26</code> <a href="/api/v1/image/date/2026-01-26" target="_blank" class="preview-link">立即预览</a></li>
|
||
</ul>
|
||
<p>参数支持:<code>variant=UHD|1920x1080|1366x768</code>,<code>format=jpg</code>。</p>
|
||
</div>
|
||
<div id="gallery" class="gallery">
|
||
<div class="loading">正在加载图片...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content-login" class="container hidden">
|
||
<div class="form-card">
|
||
<h2>管理员登录</h2>
|
||
<input type="password" id="login-password" placeholder="请输入管理员密码" onkeypress="if(event.keyCode==13) login()">
|
||
<button onclick="login()">登录</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content-admin" class="container hidden">
|
||
<div class="admin-grid">
|
||
<div class="left-col">
|
||
<div class="admin-card">
|
||
<h3>Token 管理</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>名称</th>
|
||
<th>过期时间</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="token-list"></tbody>
|
||
</table>
|
||
<h4 style="margin-top: 2rem;">创建新 Token</h4>
|
||
<div style="display: flex; gap: 10px;">
|
||
<input type="text" id="token-name" placeholder="Token 名称" style="margin: 0;">
|
||
<button onclick="createToken()" style="width: auto; padding: 0 20px;">创建</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-card">
|
||
<h3>配置文件</h3>
|
||
<pre id="config-view"></pre>
|
||
<button class="secondary" onclick="loadConfig()">刷新配置</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="right-col">
|
||
<div class="admin-card">
|
||
<h3>任务控制</h3>
|
||
<button onclick="triggerFetch()" style="margin-bottom: 10px;">手动抓取 (最近8天)</button>
|
||
<button class="secondary" onclick="triggerCleanup()" style="margin-bottom: 10px;">手动清理旧图</button>
|
||
</div>
|
||
|
||
<div class="admin-card">
|
||
<h3>修改密码</h3>
|
||
<input type="password" id="old-password" placeholder="旧密码" style="margin-bottom: 10px;">
|
||
<input type="password" id="new-password" placeholder="新密码" style="margin-bottom: 10px;">
|
||
<button onclick="changePassword()">提交修改</button>
|
||
</div>
|
||
|
||
<div class="admin-card">
|
||
<h3>今日图预览</h3>
|
||
<div id="today-preview" style="text-align: center;">
|
||
<div class="loading" style="padding: 1rem;">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let token = localStorage.getItem('bing_token');
|
||
|
||
// 简易路由实现
|
||
function router() {
|
||
const path = window.location.pathname;
|
||
const homeSection = document.getElementById('content-home');
|
||
const loginSection = document.getElementById('content-login');
|
||
const adminSection = document.getElementById('content-admin');
|
||
const logoutNav = document.getElementById('nav-logout-container');
|
||
|
||
// 隐藏所有
|
||
[homeSection, loginSection, adminSection].forEach(s => s.classList.add('hidden'));
|
||
logoutNav.classList.add('hidden');
|
||
|
||
if (path === '/' || path === '') {
|
||
homeSection.classList.remove('hidden');
|
||
loadGallery();
|
||
} else if (path === '/login') {
|
||
if (token) {
|
||
window.history.pushState({}, '', '/admin');
|
||
router();
|
||
return;
|
||
}
|
||
loginSection.classList.remove('hidden');
|
||
} else if (path === '/admin') {
|
||
if (!token) {
|
||
window.history.pushState({}, '', '/login');
|
||
router();
|
||
return;
|
||
}
|
||
adminSection.classList.remove('hidden');
|
||
logoutNav.classList.remove('hidden');
|
||
loadAdminData();
|
||
} else {
|
||
// 404 默认首页
|
||
window.history.pushState({}, '', '/');
|
||
router();
|
||
}
|
||
}
|
||
|
||
// 处理导航点击
|
||
document.querySelectorAll('nav a').forEach(a => {
|
||
if (a.onclick) return;
|
||
a.addEventListener('click', e => {
|
||
if (a.getAttribute('href').startsWith('http')) return;
|
||
e.preventDefault();
|
||
const href = a.getAttribute('href');
|
||
window.history.pushState({}, '', href);
|
||
router();
|
||
});
|
||
});
|
||
|
||
// 监听后退/前进
|
||
window.addEventListener('popstate', router);
|
||
|
||
async function login() {
|
||
const password = document.getElementById('login-password').value;
|
||
try {
|
||
const resp = await fetch('/api/v1/admin/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ password })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.token) {
|
||
token = data.token;
|
||
localStorage.setItem('bing_token', token);
|
||
window.history.pushState({}, '', '/admin');
|
||
router();
|
||
} else {
|
||
alert('登录失败: ' + (data.error || '未知错误'));
|
||
}
|
||
} catch (e) {
|
||
alert('网络请求失败');
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('bing_token');
|
||
token = null;
|
||
window.history.pushState({}, '', '/');
|
||
router();
|
||
}
|
||
|
||
// 画廊功能
|
||
async function loadGallery() {
|
||
const gallery = document.getElementById('gallery');
|
||
try {
|
||
const resp = await fetch('/api/v1/images?limit=30');
|
||
const data = await resp.json();
|
||
|
||
if (data.length === 0) {
|
||
gallery.innerHTML = '<div class="loading">暂无图片,请稍后再试。</div>';
|
||
return;
|
||
}
|
||
|
||
gallery.innerHTML = data.map(img => `
|
||
<div class="gallery-item">
|
||
<div class="date">${img.date}</div>
|
||
<img src="${img.variants.find(v => v.variant === '1920x1080')?.url || img.variants[0].url}" alt="${img.title}" loading="lazy">
|
||
<div class="overlay">
|
||
<div class="title">${img.title}</div>
|
||
<div class="copyright">${img.copyright}</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
gallery.innerHTML = '<div class="loading">加载失败,请刷新重试。</div>';
|
||
}
|
||
}
|
||
|
||
// 管理员数据
|
||
function loadAdminData() {
|
||
loadTokens();
|
||
loadConfig();
|
||
loadTodayPreview();
|
||
}
|
||
|
||
async function loadTokens() {
|
||
const resp = await fetch('/api/v1/admin/tokens', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
if (resp.status === 401) return logout();
|
||
const data = await resp.json();
|
||
const tbody = document.getElementById('token-list');
|
||
tbody.innerHTML = data.map(t => `
|
||
<tr>
|
||
<td>${t.name}</td>
|
||
<td>${new Date(t.expires_at).toLocaleString()}</td>
|
||
<td><span class="status-badge ${t.disabled ? 'status-disabled' : 'status-enabled'}">${t.disabled ? '禁用' : '启用'}</span></td>
|
||
<td>
|
||
<button style="width:auto; padding:5px 10px; margin-right:5px;" onclick="toggleToken(${t.id}, ${!t.disabled})">${t.disabled ? '启用' : '禁用'}</button>
|
||
<button class="danger" style="width:auto; padding:5px 10px;" onclick="deleteToken(${t.id})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
async function loadConfig() {
|
||
const resp = await fetch('/api/v1/admin/config', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
const data = await resp.json();
|
||
document.getElementById('config-view').innerText = JSON.stringify(data, null, 2);
|
||
}
|
||
|
||
async function loadTodayPreview() {
|
||
const resp = await fetch('/api/v1/image/today/meta');
|
||
const data = await resp.json();
|
||
const container = document.getElementById('today-preview');
|
||
container.innerHTML = `
|
||
<p><strong>${data.date}</strong></p>
|
||
<p>${data.title}</p>
|
||
<img src="${data.variants[0].url}" style="max-width:100%; border-radius:8px;">
|
||
`;
|
||
}
|
||
|
||
async function createToken() {
|
||
const name = document.getElementById('token-name').value;
|
||
if (!name) return alert('请输入名称');
|
||
await fetch('/api/v1/admin/tokens', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name })
|
||
});
|
||
document.getElementById('token-name').value = '';
|
||
loadTokens();
|
||
}
|
||
|
||
async function toggleToken(id, disabled) {
|
||
await fetch('/api/v1/admin/tokens/' + id, {
|
||
method: 'PATCH',
|
||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ disabled })
|
||
});
|
||
loadTokens();
|
||
}
|
||
|
||
async function deleteToken(id) {
|
||
if (!confirm('确定删除?')) return;
|
||
await fetch('/api/v1/admin/tokens/' + id, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
loadTokens();
|
||
}
|
||
|
||
async function triggerFetch() {
|
||
await fetch('/api/v1/admin/fetch', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ n: 8 })
|
||
});
|
||
alert('抓取任务已启动');
|
||
}
|
||
|
||
async function triggerCleanup() {
|
||
await fetch('/api/v1/admin/cleanup', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
alert('清理任务已启动');
|
||
}
|
||
|
||
async function changePassword() {
|
||
const oldPassword = document.getElementById('old-password').value;
|
||
const newPassword = document.getElementById('new-password').value;
|
||
if (!oldPassword || !newPassword) return alert('请输入完整信息');
|
||
|
||
const resp = await fetch('/api/v1/admin/password', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
|
||
});
|
||
|
||
const data = await resp.json();
|
||
if (resp.ok) {
|
||
alert('密码修改成功,请重新登录');
|
||
logout();
|
||
} else {
|
||
alert('修改失败: ' + (data.error || '未知错误'));
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
router();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|