This commit is contained in:
2026-03-11 20:41:03 +08:00
commit c5af0cc946
21 changed files with 5831 additions and 0 deletions

420
cf-nav-frontend/admin.js Normal file
View File

@@ -0,0 +1,420 @@
// 后台管理 JavaScriptAPI 地址从 config.js 读取token 仅从 URL ?token= 传入并由后端校验)
const API_BASE = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : '';
// 从 URL 读取 token不在前端校验直接交给后端无 token 则不显示后台
const urlParams = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
const authToken = urlParams.get('token') || null;
let categories = [];
let allSites = [];
// 显示提示消息
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.className = `toast ${type} show`;
document.querySelector('.toast-message').textContent = message;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 显示无权限提示(无 token 或 token 错误)
function showNoPermission() {
const noPerm = document.getElementById('no-permission');
const adminEl = document.getElementById('admin-container');
if (noPerm) noPerm.style.display = 'block';
if (adminEl) adminEl.style.display = 'none';
}
// 退出:跳转到当前页不带 query下次进入需重新带 token
document.getElementById('logout-btn').addEventListener('click', () => {
if (typeof location !== 'undefined') location.href = location.pathname;
});
// 显示管理面板(仅 token 正确时)
function showAdminPanel() {
const noPerm = document.getElementById('no-permission');
const adminEl = document.getElementById('admin-container');
if (noPerm) noPerm.style.display = 'none';
if (adminEl) adminEl.style.display = 'block';
loadSites();
loadCategories();
}
// 进入页时先向后端校验 token通过才显示后台
async function initAdmin() {
if (!authToken) {
showNoPermission();
return;
}
try {
const response = await fetch(`${API_BASE}/api/auth/check`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.status !== 200) {
showNoPermission();
return;
}
const data = await response.json().catch(() => ({}));
if (!data || !data.ok) {
showNoPermission();
return;
}
showAdminPanel();
} catch (_) {
showNoPermission();
}
}
// 加载分类列表
async function loadCategories() {
try {
const response = await fetch(`${API_BASE}/api/categories`, {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
});
if (response.status === 401) {
showNoPermission();
return;
}
categories = response.ok ? await response.json() : [];
renderCategoryList();
updateCategorySelect();
updateSiteCategoryFilterOptions();
if (allSites.length) renderSites(allSites);
} catch (error) {
categories = [];
renderCategoryList();
}
}
// 渲染分类标签
function renderCategoryList() {
const container = document.getElementById('category-list');
container.innerHTML = '';
if (!categories.length) {
const empty = document.createElement('div');
empty.style.color = '#64748b';
empty.textContent = '暂无分类';
container.appendChild(empty);
return;
}
categories.forEach(name => {
const item = document.createElement('div');
item.className = 'category-item';
item.innerHTML = `
<span>${name}</span>
<span class="category-actions">
<button onclick="editCategory('${name.replace(/'/g, "\\'")}')">编辑</button>
<button onclick="deleteCategory('${name.replace(/'/g, "\\'")}')">删除</button>
</span>
`;
container.appendChild(item);
});
}
// 更新分类下拉
function updateCategorySelect(selectedValue = '') {
const select = document.getElementById('edit-site-category');
if (!select) {
return;
}
const options = ['默认'];
const merged = Array.from(new Set([...options, ...categories].filter(Boolean)));
select.innerHTML = '<option value="">选择分类</option>';
merged.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
if (name === selectedValue) {
option.selected = true;
}
select.appendChild(option);
});
}
// 更新「网站列表」上方的分类筛选下拉
function updateSiteCategoryFilterOptions() {
const select = document.getElementById('site-category-filter');
if (!select) return;
const current = select.value;
const fromSites = (allSites || []).map(s => s.category || '默认').filter(Boolean);
const combined = Array.from(new Set(['默认', ...categories, ...fromSites]));
select.innerHTML = '<option value="">全部</option>';
combined.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});
select.value = current || '';
}
// 根据当前筛选条件渲染网站表格
function renderSites(sites) {
const tbody = document.getElementById('sites-tbody');
const filterEl = document.getElementById('site-category-filter');
const categoryFilter = filterEl ? filterEl.value : '';
const list = categoryFilter
? sites.filter(site => (site.category || '默认') === categoryFilter)
: sites;
tbody.innerHTML = '';
list.forEach(site => {
const tr = document.createElement('tr');
const tagsHtml = site.tags && site.tags.length > 0
? `<div class="tag-display">${site.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}</div>`
: '-';
tr.innerHTML = `
<td><strong>${site.name}</strong></td>
<td><a href="${site.url}" target="_blank" style="color: #3b82f6;">${site.url}</a></td>
<td>${site.category}</td>
<td>${site.description || '-'}</td>
<td>${tagsHtml}</td>
<td>
<div class="action-btns">
<button class="btn-edit" onclick="editSite('${site.id}')">编辑</button>
<button class="btn-delete" onclick="deleteSite('${site.id}')">删除</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
}
// 加载网站列表
async function loadSites() {
try {
const response = await fetch(`${API_BASE}/api/sites`, {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
});
if (response.status === 401) {
showNoPermission();
return;
}
const sites = await response.json();
allSites = sites || [];
updateSiteCategoryFilterOptions();
renderSites(allSites);
} catch (error) {
showToast('加载网站列表失败', 'error');
}
}
// 网站列表按分类筛选
const siteCategoryFilterEl = document.getElementById('site-category-filter');
if (siteCategoryFilterEl) {
siteCategoryFilterEl.addEventListener('change', () => {
renderSites(allSites);
});
}
// 添加新网站
document.getElementById('add-new-site').addEventListener('click', () => {
if (!authToken) return;
document.getElementById('modal-title').textContent = '添加新网站';
document.getElementById('edit-site-id').value = '';
document.getElementById('edit-site-form').reset();
updateCategorySelect();
document.getElementById('edit-modal').classList.add('active');
});
// 编辑网站
async function editSite(id) {
try {
const response = await fetch(`${API_BASE}/api/sites/${id}`);
const site = await response.json();
document.getElementById('modal-title').textContent = '编辑网站';
document.getElementById('edit-site-id').value = site.id;
document.getElementById('edit-site-name').value = site.name;
document.getElementById('edit-site-url').value = site.url;
document.getElementById('edit-site-description').value = site.description || '';
updateCategorySelect(site.category);
document.getElementById('edit-site-tags').value = site.tags ? site.tags.join(', ') : '';
document.getElementById('edit-modal').classList.add('active');
} catch (error) {
showToast('加载网站信息失败', 'error');
}
}
// 删除网站
async function deleteSite(id) {
if (!confirm('确定要删除这个网站吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/sites/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('网站删除成功');
loadSites();
} else {
showToast('删除失败', 'error');
}
} catch (error) {
showToast('删除请求失败', 'error');
}
}
// 保存网站(添加或更新)
document.getElementById('edit-site-form').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('edit-site-id').value;
const site = {
name: document.getElementById('edit-site-name').value.trim(),
url: document.getElementById('edit-site-url').value.trim(),
description: document.getElementById('edit-site-description').value.trim(),
category: document.getElementById('edit-site-category').value,
tags: document.getElementById('edit-site-tags').value
.split(',')
.map(tag => tag.trim())
.filter(tag => tag)
};
// 验证URL
if (!site.url.startsWith('http://') && !site.url.startsWith('https://')) {
site.url = 'https://' + site.url;
}
try {
const url = id ? `${API_BASE}/api/sites/${id}` : `${API_BASE}/api/sites`;
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(site)
});
if (response.ok) {
showToast(id ? '网站更新成功' : '网站添加成功');
document.getElementById('edit-modal').classList.remove('active');
loadSites();
} else {
showToast('保存失败', 'error');
}
} catch (error) {
showToast('保存请求失败', 'error');
}
});
// 关闭模态框
document.getElementById('close-edit-modal').addEventListener('click', () => {
document.getElementById('edit-modal').classList.remove('active');
});
document.getElementById('edit-modal').addEventListener('click', (e) => {
if (e.target.id === 'edit-modal') {
document.getElementById('edit-modal').classList.remove('active');
}
});
// 添加分类
document.getElementById('add-category-btn').addEventListener('click', async () => {
const input = document.getElementById('new-category-name');
const name = input.value.trim();
if (!name) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ name })
});
if (response.ok) {
input.value = '';
await loadCategories();
showToast('分类添加成功');
} else {
showToast('分类添加失败', 'error');
}
} catch (error) {
showToast('分类添加失败', 'error');
}
});
// 编辑分类
async function editCategory(oldName) {
const newName = prompt('请输入新的分类名称', oldName);
if (!newName || newName.trim() === oldName) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/categories/${encodeURIComponent(oldName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ name: newName.trim() })
});
if (response.ok) {
await loadCategories();
await loadSites();
showToast('分类更新成功');
} else {
showToast('分类更新失败', 'error');
}
} catch (error) {
showToast('分类更新失败', 'error');
}
}
// 删除分类
async function deleteCategory(name) {
if (!confirm(`确定删除分类「${name}」吗?`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/categories/${encodeURIComponent(name)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
await loadCategories();
showToast('分类删除成功');
} else {
showToast('分类删除失败', 'error');
}
} catch (error) {
showToast('分类删除失败', 'error');
}
}
// 初始化
initAdmin();