487 lines
22 KiB
HTML
487 lines
22 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>UAPI管理系统</title>
|
|||
|
|
<style>
|
|||
|
|
:root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --muted: #8b949e; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d2991d; }
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; padding: 40px 20px; }
|
|||
|
|
.container { max-width: 960px; margin: 0 auto; }
|
|||
|
|
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; flex-wrap: wrap; gap: 16px; }
|
|||
|
|
header h1 { font-size: 24px; color: #fff; }
|
|||
|
|
header .actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|||
|
|
.btn { padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; display: inline-flex; align-items: center; gap: 6px; }
|
|||
|
|
.btn-primary { background: #238636; color: #fff; }
|
|||
|
|
.btn-primary:hover { background: #2ea043; }
|
|||
|
|
.btn-secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); }
|
|||
|
|
.btn-secondary:hover { background: #1c2333; }
|
|||
|
|
.btn-danger { background: var(--card); color: var(--red); border: 1px solid var(--red); }
|
|||
|
|
.btn-danger:hover { background: #49020233; }
|
|||
|
|
.stats { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
|
|||
|
|
.stat { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 10px 20px; }
|
|||
|
|
.stat .num { font-size: 20px; font-weight: 700; color: #fff; }
|
|||
|
|
.stat .label { font-size: 11px; color: var(--muted); }
|
|||
|
|
.stat.alive .num { color: var(--green); }
|
|||
|
|
.stat.dead .num { color: var(--red); }
|
|||
|
|
table { width: 100%; border-collapse: collapse; background: var(--card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|||
|
|
th, td { padding: 12px 16px; text-align: left; font-size: 13px; }
|
|||
|
|
th { background: #1c2333; color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; }
|
|||
|
|
td { border-top: 1px solid var(--border); }
|
|||
|
|
td.url { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: var(--accent); word-break: break-all; }
|
|||
|
|
td.title { color: #fff; font-weight: 500; display: flex; align-items: center; }
|
|||
|
|
td.actions { white-space: nowrap; }
|
|||
|
|
td.actions button { padding: 4px 10px; font-size: 12px; margin-right: 6px; }
|
|||
|
|
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; flex-shrink: 0; }
|
|||
|
|
.status-dot.alive { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|||
|
|
.status-dot.dead { background: var(--red); }
|
|||
|
|
.status-dot.checking { background: var(--yellow); animation: pulse 0.8s ease-in-out infinite; }
|
|||
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|||
|
|
.latency { font-size: 11px; color: var(--muted); margin-left: 6px; }
|
|||
|
|
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 100; align-items: center; justify-content: center; }
|
|||
|
|
.modal-overlay.active { display: flex; }
|
|||
|
|
.modal { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 32px; width: 100%; max-width: 480px; }
|
|||
|
|
.modal h2 { font-size: 18px; color: #fff; margin-bottom: 20px; }
|
|||
|
|
.modal label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; margin-top: 12px; }
|
|||
|
|
.modal input, .modal select { width: 100%; padding: 9px 12px; background: #0d1117; border: 1px solid var(--border); border-radius: 6px; color: #fff; font-size: 14px; outline: none; }
|
|||
|
|
.modal input:focus { border-color: var(--accent); }
|
|||
|
|
.modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
|
|||
|
|
.toast { position: fixed; bottom: 24px; right: 24px; background: #238636; color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 13px; z-index: 200; display: none; animation: fadeIn 0.2s ease; }
|
|||
|
|
.toast.error { background: #da3633; }
|
|||
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|||
|
|
.empty { text-align: center; padding: 60px 20px; color: var(--muted); }
|
|||
|
|
.empty h3 { font-size: 16px; color: #fff; margin-bottom: 8px; }
|
|||
|
|
.tab { display: inline-flex; gap: 2px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 4px; margin-bottom: 24px; }
|
|||
|
|
.tab button { padding: 6px 16px; border: none; background: none; color: var(--muted); border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|||
|
|
.tab button.active { background: #1f6feb; color: #fff; }
|
|||
|
|
.feed-url-bar { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 10px 16px; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
|||
|
|
.feed-url-bar code { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent); font-size: 12px; }
|
|||
|
|
.link-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; background: var(--border); }
|
|||
|
|
.link-name { color: #fff; font-weight: 500; display: flex; align-items: center; gap: 10px; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<header>
|
|||
|
|
<h1>UAPI管理系统</h1>
|
|||
|
|
<div class="actions">
|
|||
|
|
<a href="/" class="btn btn-secondary" target="_blank">API 文档</a>
|
|||
|
|
<button class="btn btn-secondary" onclick="checkHealth()" id="healthBtn">检测存活</button>
|
|||
|
|
<button class="btn btn-primary" onclick="openAddModal()">+ 添加订阅源</button>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="feed-url-bar">
|
|||
|
|
<span style="color:var(--muted)">订阅地址</span>
|
|||
|
|
<code id="feedUrlText">https://api.200181.xyz/api/feeds</code>
|
|||
|
|
<button class="btn btn-secondary" style="padding:4px 10px;font-size:11px" onclick="copyFeedUrl()">复制</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="stats">
|
|||
|
|
<div class="stat"><div class="num" id="feedCount">0</div><div class="label">订阅源</div></div>
|
|||
|
|
<div class="stat"><div class="num" id="linkCount">0</div><div class="label">友链</div></div>
|
|||
|
|
<div class="stat alive" id="aliveStat" style="display:none"><div class="num" id="aliveCount">0</div><div class="label">存活</div></div>
|
|||
|
|
<div class="stat dead" id="deadStat" style="display:none"><div class="num" id="deadCount">0</div><div class="label">异常</div></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="tab">
|
|||
|
|
<button class="active" onclick="showTab('feeds')">订阅源</button>
|
|||
|
|
<button onclick="showTab('links')">友链</button>
|
|||
|
|
<button onclick="showTab('password')">访问口令</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="tab-feeds">
|
|||
|
|
<table><thead><tr><th style="width:220px">博客名称</th><th>URL</th><th style="width:100px">操作</th></tr></thead><tbody id="feedTableBody"></tbody></table>
|
|||
|
|
<div class="empty" id="emptyMsg" style="display:none"><h3>暂无订阅源</h3><p>点击上方按钮添加第一个</p></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="tab-links" style="display:none">
|
|||
|
|
<div style="margin-bottom:16px">
|
|||
|
|
<button class="btn btn-primary" onclick="openLinkModal()">+ 添加友链</button>
|
|||
|
|
</div>
|
|||
|
|
<table><thead><tr><th style="width:50px"></th><th style="width:180px">站点名称</th><th>URL</th><th style="width:200px">描述</th><th style="width:100px">操作</th></tr></thead><tbody id="linkTableBody"></tbody></table>
|
|||
|
|
<div class="empty" id="linkEmpty" style="display:none"><h3>暂无友链</h3><p>点击上方按钮添加</p></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="tab-password" style="display:none">
|
|||
|
|
<div class="modal" style="max-width:400px; margin:0 auto; position:static; border-radius:8px;">
|
|||
|
|
<h2>修改访问口令</h2>
|
|||
|
|
<label>当前口令</label>
|
|||
|
|
<input type="password" id="oldPwd" placeholder="当前口令">
|
|||
|
|
<label>新口令(留空则清除保护)</label>
|
|||
|
|
<input type="password" id="newPwd" placeholder="新口令">
|
|||
|
|
<div class="modal-actions">
|
|||
|
|
<button class="btn btn-secondary" onclick="document.getElementById('oldPwd').value='';document.getElementById('newPwd').value='';">取消</button>
|
|||
|
|
<button class="btn btn-primary" onclick="changePassword()">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 添加/编辑弹窗 -->
|
|||
|
|
<div class="modal-overlay" id="feedModal">
|
|||
|
|
<div class="modal">
|
|||
|
|
<h2 id="modalTitle">添加订阅源</h2>
|
|||
|
|
<label>博客 URL</label>
|
|||
|
|
<input type="text" id="feedUrl" placeholder="https://example.com">
|
|||
|
|
<label>博客名称</label>
|
|||
|
|
<input type="text" id="feedTitle" placeholder="示例博客">
|
|||
|
|
<label>抓取格式</label>
|
|||
|
|
<select id="feedFormat">
|
|||
|
|
<option value="">自动检测 (auto)</option>
|
|||
|
|
<option value="xml">RSS/XML</option>
|
|||
|
|
<option value="json">JSON Feed</option>
|
|||
|
|
</select>
|
|||
|
|
<label>JSON 数据路径(format=json 时填写)</label>
|
|||
|
|
<input type="text" id="feedPath" placeholder="例如 data.list">
|
|||
|
|
<label>强制使用代理</label>
|
|||
|
|
<select id="feedProxy">
|
|||
|
|
<option value="">否</option>
|
|||
|
|
<option value="true">是</option>
|
|||
|
|
</select>
|
|||
|
|
<div class="modal-actions">
|
|||
|
|
<button class="btn btn-secondary" onclick="closeModal()">取消</button>
|
|||
|
|
<button class="btn btn-primary" id="saveBtn" onclick="saveFeed()">保存</button>
|
|||
|
|
<button class="btn btn-danger" id="deleteBtn" style="display:none" onclick="deleteFeed()">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 友链弹窗 -->
|
|||
|
|
<div class="modal-overlay" id="linkModal">
|
|||
|
|
<div class="modal">
|
|||
|
|
<h2 id="linkModalTitle">添加友链</h2>
|
|||
|
|
<label>站点名称</label>
|
|||
|
|
<input type="text" id="linkName" placeholder="示例博客">
|
|||
|
|
<label>站点 URL</label>
|
|||
|
|
<input type="text" id="linkUrl" placeholder="https://example.com">
|
|||
|
|
<label>头像图片 URL(留空自动获取 favicon)</label>
|
|||
|
|
<input type="text" id="linkImage" placeholder="自动获取">
|
|||
|
|
<label>描述</label>
|
|||
|
|
<input type="text" id="linkDesc" placeholder="一个很棒的博客">
|
|||
|
|
<label>RSS 订阅地址(选填)</label>
|
|||
|
|
<input type="text" id="linkRss" placeholder="https://example.com/feed">
|
|||
|
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:12px">
|
|||
|
|
<input type="checkbox" id="linkHidden" style="width:auto;accent-color:var(--red)"> 隐藏(不在公开接口展示)
|
|||
|
|
</label>
|
|||
|
|
<div class="modal-actions">
|
|||
|
|
<button class="btn btn-secondary" onclick="closeLinkModal()">取消</button>
|
|||
|
|
<button class="btn btn-primary" id="linkSaveBtn" onclick="saveLink()">保存</button>
|
|||
|
|
<button class="btn btn-danger" id="linkDeleteBtn" style="display:none" onclick="deleteLink()">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="toast" id="toast"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let token = '';
|
|||
|
|
const cMatch = document.cookie.match(/site_token=([^;]+)/);
|
|||
|
|
if (cMatch) token = cMatch[1];
|
|||
|
|
if (!token) { location.href = '/feed'; }
|
|||
|
|
|
|||
|
|
let feeds = [];
|
|||
|
|
let editingUrl = null;
|
|||
|
|
|
|||
|
|
function showToast(msg, isError) {
|
|||
|
|
const t = document.getElementById('toast');
|
|||
|
|
t.textContent = msg;
|
|||
|
|
t.className = 'toast' + (isError ? ' error' : '');
|
|||
|
|
t.style.display = 'block';
|
|||
|
|
setTimeout(() => t.style.display = 'none', 2500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function copyFeedUrl() {
|
|||
|
|
navigator.clipboard.writeText('https://api.200181.xyz/api/feeds').then(() => showToast('已复制订阅地址'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showTab(name) {
|
|||
|
|
document.querySelectorAll('.tab button').forEach((b, i) => b.classList.toggle('active', (i === 0 && name === 'feeds') || (i === 1 && name === 'links') || (i === 2 && name === 'password')));
|
|||
|
|
document.getElementById('tab-feeds').style.display = name === 'feeds' ? '' : 'none';
|
|||
|
|
document.getElementById('tab-links').style.display = name === 'links' ? '' : 'none';
|
|||
|
|
document.getElementById('tab-password').style.display = name === 'password' ? '' : 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderTable(healthMap) {
|
|||
|
|
const tbody = document.getElementById('feedTableBody');
|
|||
|
|
if (feeds.length === 0) {
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
document.getElementById('emptyMsg').style.display = '';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
document.getElementById('emptyMsg').style.display = 'none';
|
|||
|
|
tbody.innerHTML = feeds.map((f, i) => {
|
|||
|
|
const h = healthMap ? healthMap[f.url] : null;
|
|||
|
|
let dot = '';
|
|||
|
|
let latency = '';
|
|||
|
|
if (h) {
|
|||
|
|
if (h.ok) {
|
|||
|
|
dot = '<span class="status-dot alive" title="HTTP ' + h.status + '"></span>';
|
|||
|
|
latency = '<span class="latency">' + h.latency + 'ms</span>';
|
|||
|
|
} else {
|
|||
|
|
dot = '<span class="status-dot dead" title="' + (h.error || 'HTTP ' + h.status) + '"></span>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return '<tr>' +
|
|||
|
|
'<td class="title">' + dot + (f.feedTitle || '-') + latency + '</td>' +
|
|||
|
|
'<td class="url">' + f.url + '</td>' +
|
|||
|
|
'<td class="actions"><button class="btn btn-secondary" onclick="openEditModal(' + i + ')">编辑</button></td>' +
|
|||
|
|
'</tr>';
|
|||
|
|
}).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadFeeds() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/feeds?token=' + encodeURIComponent(token));
|
|||
|
|
if (res.status === 401) { location.href = '/feed'; return; }
|
|||
|
|
const data = await res.json();
|
|||
|
|
feeds = data.feeds || [];
|
|||
|
|
document.getElementById('feedCount').textContent = feeds.length;
|
|||
|
|
renderTable(null);
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('加载失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function checkHealth() {
|
|||
|
|
const btn = document.getElementById('healthBtn');
|
|||
|
|
if (feeds.length === 0) { showToast('暂无订阅源', true); return; }
|
|||
|
|
btn.textContent = '检测中 0/' + feeds.length;
|
|||
|
|
btn.disabled = true;
|
|||
|
|
const healthMap = {};
|
|||
|
|
let done = 0;
|
|||
|
|
// 每批 5 个并发,逐批更新 UI
|
|||
|
|
for (let i = 0; i < feeds.length; i += 5) {
|
|||
|
|
const batch = feeds.slice(i, i + 5);
|
|||
|
|
await Promise.all(batch.map(async (f) => {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/health?url=' + encodeURIComponent(f.url));
|
|||
|
|
healthMap[f.url] = await res.json();
|
|||
|
|
} catch {
|
|||
|
|
healthMap[f.url] = { ok: false, error: 'network error' };
|
|||
|
|
}
|
|||
|
|
done++;
|
|||
|
|
btn.textContent = '检测中 ' + done + '/' + feeds.length;
|
|||
|
|
}));
|
|||
|
|
renderTable(healthMap);
|
|||
|
|
}
|
|||
|
|
const alive = Object.values(healthMap).filter(r => r.ok).length;
|
|||
|
|
const dead = Object.values(healthMap).filter(r => !r.ok).length;
|
|||
|
|
document.getElementById('aliveStat').style.display = '';
|
|||
|
|
document.getElementById('deadStat').style.display = '';
|
|||
|
|
document.getElementById('aliveCount').textContent = alive;
|
|||
|
|
document.getElementById('deadCount').textContent = dead;
|
|||
|
|
btn.textContent = '检测存活';
|
|||
|
|
btn.disabled = false;
|
|||
|
|
showToast('存活 ' + alive + ' / 异常 ' + dead);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openAddModal() {
|
|||
|
|
editingUrl = null;
|
|||
|
|
document.getElementById('modalTitle').textContent = '添加订阅源';
|
|||
|
|
document.getElementById('feedUrl').value = '';
|
|||
|
|
document.getElementById('feedTitle').value = '';
|
|||
|
|
document.getElementById('feedFormat').value = '';
|
|||
|
|
document.getElementById('feedPath').value = '';
|
|||
|
|
document.getElementById('feedProxy').value = '';
|
|||
|
|
document.getElementById('deleteBtn').style.display = 'none';
|
|||
|
|
document.getElementById('feedModal').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openEditModal(idx) {
|
|||
|
|
const f = feeds[idx];
|
|||
|
|
editingUrl = f.url;
|
|||
|
|
document.getElementById('modalTitle').textContent = '编辑订阅源';
|
|||
|
|
document.getElementById('feedUrl').value = f.url || '';
|
|||
|
|
document.getElementById('feedTitle').value = f.feedTitle || '';
|
|||
|
|
document.getElementById('feedFormat').value = f.format || '';
|
|||
|
|
document.getElementById('feedPath').value = f.path || '';
|
|||
|
|
document.getElementById('feedProxy').value = f.proxy ? 'true' : '';
|
|||
|
|
document.getElementById('deleteBtn').style.display = '';
|
|||
|
|
document.getElementById('feedModal').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
document.getElementById('feedModal').classList.remove('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveFeed() {
|
|||
|
|
const url = document.getElementById('feedUrl').value.trim();
|
|||
|
|
const feedTitle = document.getElementById('feedTitle').value.trim();
|
|||
|
|
if (!url) { showToast('URL 不能为空', true); return; }
|
|||
|
|
const body = { url };
|
|||
|
|
if (feedTitle) body.feedTitle = feedTitle;
|
|||
|
|
const format = document.getElementById('feedFormat').value;
|
|||
|
|
if (format) body.format = format;
|
|||
|
|
const path = document.getElementById('feedPath').value.trim();
|
|||
|
|
if (path) body.path = path;
|
|||
|
|
const proxy = document.getElementById('feedProxy').value;
|
|||
|
|
if (proxy) body.proxy = true;
|
|||
|
|
if (editingUrl) body.oldUrl = editingUrl;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/feeds?token=' + encodeURIComponent(token), {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(body)
|
|||
|
|
});
|
|||
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|||
|
|
closeModal();
|
|||
|
|
showToast(editingUrl ? '已更新' : '已添加');
|
|||
|
|
loadFeeds();
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('保存失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteFeed() {
|
|||
|
|
if (!editingUrl || !confirm('确定删除 ' + editingUrl + ' ?')) return;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/feeds?token=' + encodeURIComponent(token) + '&url=' + encodeURIComponent(editingUrl), { method: 'DELETE' });
|
|||
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|||
|
|
closeModal();
|
|||
|
|
showToast('已删除');
|
|||
|
|
loadFeeds();
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('删除失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 友链管理 ───
|
|||
|
|
let links = [];
|
|||
|
|
let editingLinkUrl = null;
|
|||
|
|
|
|||
|
|
function renderLinkTable() {
|
|||
|
|
const tbody = document.getElementById('linkTableBody');
|
|||
|
|
if (links.length === 0) {
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
document.getElementById('linkEmpty').style.display = '';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
document.getElementById('linkEmpty').style.display = 'none';
|
|||
|
|
tbody.innerHTML = links.map((l, i) => {
|
|||
|
|
var img = l.image || l.avatar || ('https://api.200181.xyz/api/favicon?url=' + encodeURIComponent(l.url));
|
|||
|
|
var hiddenBadge = l.hidden ? ' <span style="font-size:10px;color:var(--red);border:1px solid var(--red);border-radius:4px;padding:1px 5px;margin-left:6px">隐藏</span>' : '';
|
|||
|
|
return '<tr>' +
|
|||
|
|
'<td><img src="' + img + '" class="link-avatar" onerror="this.style.opacity=0"></td>' +
|
|||
|
|
'<td class="link-name">' + (l.name || '-') + hiddenBadge + '</td>' +
|
|||
|
|
'<td class="url">' + l.url + '</td>' +
|
|||
|
|
'<td style="font-size:12px;color:var(--muted)">' + (l.description || '') + '</td>' +
|
|||
|
|
'<td class="actions"><button class="btn btn-secondary" onclick="openEditLink(' + i + ')">编辑</button></td>' +
|
|||
|
|
'</tr>';
|
|||
|
|
}).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadLinks() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/links?all=1');
|
|||
|
|
const data = await res.json();
|
|||
|
|
links = data.links || [];
|
|||
|
|
document.getElementById('linkCount').textContent = links.length;
|
|||
|
|
renderLinkTable();
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('加载友链失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openLinkModal() {
|
|||
|
|
editingLinkUrl = null;
|
|||
|
|
document.getElementById('linkModalTitle').textContent = '添加友链';
|
|||
|
|
document.getElementById('linkName').value = '';
|
|||
|
|
document.getElementById('linkUrl').value = '';
|
|||
|
|
document.getElementById('linkImage').value = '';
|
|||
|
|
document.getElementById('linkDesc').value = '';
|
|||
|
|
document.getElementById('linkRss').value = '';
|
|||
|
|
document.getElementById('linkHidden').checked = false;
|
|||
|
|
document.getElementById('linkDeleteBtn').style.display = 'none';
|
|||
|
|
document.getElementById('linkModal').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openEditLink(idx) {
|
|||
|
|
const l = links[idx];
|
|||
|
|
editingLinkUrl = l.url;
|
|||
|
|
document.getElementById('linkModalTitle').textContent = '编辑友链';
|
|||
|
|
document.getElementById('linkName').value = l.name || '';
|
|||
|
|
document.getElementById('linkUrl').value = l.url || '';
|
|||
|
|
document.getElementById('linkImage').value = l.image || '';
|
|||
|
|
document.getElementById('linkDesc').value = l.description || '';
|
|||
|
|
document.getElementById('linkRss').value = l.rss || '';
|
|||
|
|
document.getElementById('linkHidden').checked = !!l.hidden;
|
|||
|
|
document.getElementById('linkDeleteBtn').style.display = '';
|
|||
|
|
document.getElementById('linkModal').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeLinkModal() {
|
|||
|
|
document.getElementById('linkModal').classList.remove('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveLink() {
|
|||
|
|
const name = document.getElementById('linkName').value.trim();
|
|||
|
|
const url = document.getElementById('linkUrl').value.trim();
|
|||
|
|
if (!name && !url) { showToast('名称和 URL 至少填一个', true); return; }
|
|||
|
|
if (!url) { showToast('URL 不能为空', true); return; }
|
|||
|
|
const body = { name: name || url, url: url };
|
|||
|
|
const image = document.getElementById('linkImage').value.trim();
|
|||
|
|
if (image) body.image = image;
|
|||
|
|
const desc = document.getElementById('linkDesc').value.trim();
|
|||
|
|
if (desc) body.description = desc;
|
|||
|
|
const rss = document.getElementById('linkRss').value.trim();
|
|||
|
|
if (rss) body.rss = rss;
|
|||
|
|
body.hidden = document.getElementById('linkHidden').checked;
|
|||
|
|
if (editingLinkUrl) body.oldUrl = editingLinkUrl;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/links?token=' + encodeURIComponent(token), {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(body)
|
|||
|
|
});
|
|||
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|||
|
|
closeLinkModal();
|
|||
|
|
showToast(editingLinkUrl ? '已更新' : '已添加');
|
|||
|
|
loadLinks();
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('保存失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteLink() {
|
|||
|
|
if (!editingLinkUrl || !confirm('确定删除 ' + editingLinkUrl + ' ?')) return;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/links?token=' + encodeURIComponent(token) + '&url=' + encodeURIComponent(editingLinkUrl), { method: 'DELETE' });
|
|||
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|||
|
|
closeLinkModal();
|
|||
|
|
showToast('已删除');
|
|||
|
|
loadLinks();
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('删除失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function changePassword() {
|
|||
|
|
const oldPwd = document.getElementById('oldPwd').value;
|
|||
|
|
const newPwd = document.getElementById('newPwd').value;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/auth?token=' + encodeURIComponent(token), {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ oldPassword: oldPwd, newPassword: newPwd })
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
if (!res.ok) throw new Error(data.error || 'HTTP ' + res.status);
|
|||
|
|
if (newPwd) {
|
|||
|
|
token = newPwd;
|
|||
|
|
document.cookie = 'site_token=' + newPwd + ';path=/;max-age=2592000;SameSite=Lax';
|
|||
|
|
}
|
|||
|
|
document.getElementById('oldPwd').value = '';
|
|||
|
|
document.getElementById('newPwd').value = '';
|
|||
|
|
showToast(newPwd ? '口令已更新' : '口令保护已清除');
|
|||
|
|
} catch (err) {
|
|||
|
|
showToast('修改失败: ' + err.message, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadFeeds();
|
|||
|
|
loadLinks();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|