421 lines
14 KiB
JavaScript
421 lines
14 KiB
JavaScript
// 后台管理 JavaScript(API 地址从 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();
|