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

498
cf-nav-frontend/app.js Normal file
View File

@@ -0,0 +1,498 @@
// API 配置(从 config.js 读取,部署 Pages 时请设置后端 Worker 地址)
const API_BASE = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : '';
// 初始数据(示例)
let sites = [
{
id: '1',
name: '百度',
url: 'https://www.baidu.com',
description: '全球最大的中文搜索引擎',
category: '常用',
tags: ['搜索', '中文']
},
{
id: '2',
name: '知乎',
url: 'https://www.zhihu.com',
description: '中文互联网高质量的问答社区',
category: '社交',
tags: ['问答', '知识']
},
{
id: '3',
name: 'GitHub',
url: 'https://github.com',
description: '全球最大的代码托管平台',
category: '工作',
tags: ['代码', '开发']
},
{
id: '4',
name: 'Bilibili',
url: 'https://www.bilibili.com',
description: '中国年轻世代高度聚集的文化社区',
category: '娱乐',
tags: ['视频', '弹幕']
},
{
id: '5',
name: '淘宝',
url: 'https://www.taobao.com',
description: '亚洲最大的购物网站',
category: '购物',
tags: ['电商', '购物']
}
];
let deferredInstallPrompt = null;
let installBtn = null;
let hasRefreshing = false;
function isStandaloneMode() {
return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
}
function createInstallButton() {
if (installBtn) {
return installBtn;
}
installBtn = document.createElement('button');
installBtn.className = 'install-btn';
installBtn.type = 'button';
installBtn.textContent = '📲 安装应用';
installBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 50px;
cursor: pointer;
font-weight: 600;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
display: none;
z-index: 1000;
transition: all 0.2s ease;
`;
installBtn.addEventListener('mouseenter', () => {
installBtn.style.transform = 'translateY(-2px)';
installBtn.style.boxShadow = '0 6px 20px rgba(16, 185, 129, 0.4)';
});
installBtn.addEventListener('mouseleave', () => {
installBtn.style.transform = 'translateY(0)';
installBtn.style.boxShadow = '0 4px 15px rgba(16, 185, 129, 0.3)';
});
installBtn.addEventListener('click', async () => {
if (!deferredInstallPrompt) {
return;
}
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice;
if (choice.outcome === 'accepted') {
showToast('已触发安装流程');
}
deferredInstallPrompt = null;
installBtn.style.display = 'none';
});
document.body.appendChild(installBtn);
return installBtn;
}
function initInstallPrompt() {
const button = createInstallButton();
if (isStandaloneMode()) {
button.style.display = 'none';
return;
}
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredInstallPrompt = event;
button.style.display = 'block';
});
window.addEventListener('appinstalled', () => {
deferredInstallPrompt = null;
button.style.display = 'none';
showToast('应用安装成功');
});
}
async function registerServiceWorker() {
if (!('serviceWorker' in navigator)) {
return;
}
try {
const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
if (registration.waiting) {
promptRefresh(registration.waiting);
}
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
}
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
promptRefresh(newWorker);
}
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (hasRefreshing) {
return;
}
hasRefreshing = true;
window.location.reload();
});
} catch (error) {
console.error('Service Worker 注册失败:', error);
}
}
function promptRefresh(worker) {
const shouldRefresh = window.confirm('发现新版本,是否立即刷新?');
if (shouldRefresh) {
worker.postMessage('SKIP_WAITING');
}
}
// 加载网站数据
async function loadSites() {
try {
const response = await fetch(`${API_BASE}/api/sites`);
if (response.ok) {
sites = await response.json();
updateStats();
renderSites();
}
} catch (error) {
console.error('加载网站数据失败:', error);
// 使用默认数据
updateStats();
renderSites();
}
}
// 更新统计信息
function updateStats() {
const totalSites = sites.length;
const categories = [...new Set(sites.map(site => site.category))];
const allTags = sites.flatMap(site => site.tags || []);
const uniqueTags = [...new Set(allTags)];
document.getElementById('total-sites').textContent = totalSites;
document.getElementById('total-categories').textContent = categories.length;
document.getElementById('total-tags').textContent = uniqueTags.length;
// 更新分类过滤器
updateCategoryFilters();
}
// 更新分类过滤器
function updateCategoryFilters() {
const categoryFilters = document.getElementById('category-filters');
const categories = ['all', ...new Set(sites.map(site => site.category))];
categoryFilters.innerHTML = '';
categories.forEach(category => {
const filterBtn = document.createElement('div');
filterBtn.className = 'category-filter';
filterBtn.textContent = category === 'all' ? '全部' : category;
filterBtn.dataset.category = category;
if (category === 'all') {
filterBtn.classList.add('active');
}
filterBtn.addEventListener('click', () => {
document.querySelectorAll('.category-filter').forEach(btn => {
btn.classList.remove('active');
});
filterBtn.classList.add('active');
filterSites();
closeCategorySidebar();
});
categoryFilters.appendChild(filterBtn);
});
}
// 渲染网站
function renderSites(filteredSites = null) {
const container = document.getElementById('categories-container');
const searchInput = document.getElementById('search-input').value.toLowerCase();
const activeCategory = document.querySelector('.category-filter.active').dataset.category;
// 如果没有传入过滤后的网站,则使用全部网站
const sitesToRender = filteredSites || sites;
// 如果有搜索关键词,进一步过滤
let finalSites = sitesToRender;
if (searchInput) {
finalSites = sitesToRender.filter(site =>
site.name.toLowerCase().includes(searchInput) ||
(site.description && site.description.toLowerCase().includes(searchInput)) ||
(site.tags && site.tags.some(tag => tag.toLowerCase().includes(searchInput)))
);
}
// 如果有分类过滤
if (activeCategory !== 'all') {
finalSites = finalSites.filter(site => site.category === activeCategory);
}
// 按分类分组
const sitesByCategory = {};
finalSites.forEach(site => {
if (!sitesByCategory[site.category]) {
sitesByCategory[site.category] = [];
}
sitesByCategory[site.category].push(site);
});
// 如果没有网站匹配
if (Object.keys(sitesByCategory).length === 0) {
container.innerHTML = `
<div class="empty-state">
<div style="font-size: 4rem; margin-bottom: 15px;">🔍</div>
<h2>没有找到匹配的网站</h2>
<p>尝试调整搜索关键词或分类筛选条件</p>
</div>
`;
return;
}
// 渲染分类区块
container.innerHTML = '';
Object.keys(sitesByCategory).sort().forEach(category => {
const categorySection = document.createElement('div');
categorySection.className = 'category-section';
categorySection.innerHTML = `
<h2 class="category-title">${category}</h2>
<div class="sites-grid" id="category-${category.replace(/\s+/g, '-')}">
${sitesByCategory[category].map(site => createSiteCard(site)).join('')}
</div>
`;
container.appendChild(categorySection);
});
// 添加点击事件
document.querySelectorAll('.site-card').forEach(card => {
card.addEventListener('click', (e) => {
if (!e.target.closest('.site-icon') && !e.target.closest('img')) {
window.open(card.dataset.url, '_blank');
}
});
});
}
// 通过 Worker 代理获取 favicon
function getFaviconUrl(domain) {
return `${API_BASE}/api/favicon?domain=${domain}`;
}
// 生成favicon HTML使用 Worker 代理
function generateFaviconHtml(domain, firstLetter) {
const faviconUrl = getFaviconUrl(domain);
// Worker 会自动尝试多个源,失败时显示占位符
const onerrorCode = `this.parentElement.innerHTML='<div class=\\'favicon-placeholder\\'>${firstLetter}</div>';`;
return {
src: faviconUrl,
onerror: onerrorCode
};
}
// 创建网站卡片HTML
function createSiteCard(site) {
// 从URL提取域名用于获取favicon
const domain = new URL(site.url).hostname.replace('www.', '');
// 生成网站名称首字母作为备用图标
const firstLetter = site.name.charAt(0).toUpperCase();
// 获取favicon配置
const faviconConfig = generateFaviconHtml(domain, firstLetter);
// 处理标签
const tagsHtml = site.tags && site.tags.length > 0
? site.tags.map(tag => `<span class="tag">${tag}</span>`).join('')
: '';
// 处理标签文本(用于 title
const tagsText = site.tags && site.tags.length > 0
? site.tags.join('、')
: '';
const clickCount = typeof site.clicks === 'number' ? site.clicks : 0;
return `
<a href="${site.url}" target="_blank" class="site-card" data-url="${site.url}" data-site-id="${site.id}">
<span class="site-card-clicks" title="访问次数">${clickCount}</span>
<div class="site-icon">
<img
src="${faviconConfig.src}"
onerror="${faviconConfig.onerror}"
alt="${site.name}图标"
>
</div>
<div class="site-card-body">
<div class="site-name" title="${site.name}">${site.name}</div>
<div class="site-description" title="${site.description || ''}">${site.description || ''}</div>
</div>
<div class="site-tags" title="${tagsText}">${tagsHtml}</div>
</a>
`;
}
// 显示提示消息
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);
}
// 过滤网站(搜索和分类)
function filterSites() {
renderSites();
}
// 显示提示消息
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);
}
// 过滤网站(搜索和分类)
function filterSites() {
renderSites();
}
// 网页搜索处理
function performWebSearch(query, engine) {
const encodedQuery = encodeURIComponent(query);
let searchUrl = '';
switch (engine) {
case 'google':
searchUrl = `https://www.google.com/search?q=${encodedQuery}`;
break;
case 'baidu':
searchUrl = `https://www.baidu.com/s?wd=${encodedQuery}`;
break;
case 'bing':
searchUrl = `https://www.bing.com/search?q=${encodedQuery}`;
break;
case 'duckduckgo':
searchUrl = `https://duckduckgo.com/?q=${encodedQuery}`;
break;
case 'yandex':
searchUrl = `https://yandex.com/search/?text=${encodedQuery}`;
break;
default:
searchUrl = `https://www.google.com/search?q=${encodedQuery}`;
}
window.open(searchUrl, '_blank');
}
function openCategorySidebar() {
document.body.classList.add('category-sidebar-open');
const backdrop = document.getElementById('category-sidebar-backdrop');
if (backdrop) backdrop.setAttribute('aria-hidden', 'false');
}
function closeCategorySidebar() {
document.body.classList.remove('category-sidebar-open');
const backdrop = document.getElementById('category-sidebar-backdrop');
if (backdrop) backdrop.setAttribute('aria-hidden', 'true');
}
function initCategorySidebar() {
const toggle = document.getElementById('category-sidebar-toggle');
const closeBtn = document.getElementById('category-sidebar-close');
const backdrop = document.getElementById('category-sidebar-backdrop');
if (toggle) toggle.addEventListener('click', openCategorySidebar);
if (closeBtn) closeBtn.addEventListener('click', closeCategorySidebar);
if (backdrop) backdrop.addEventListener('click', closeCategorySidebar);
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
initInstallPrompt();
initCategorySidebar();
// 点击卡片时上报访问次数(不阻止跳转)
const container = document.getElementById('categories-container');
if (container) {
container.addEventListener('click', (e) => {
const card = e.target.closest('.site-card');
if (!card) return;
const id = card.dataset.siteId;
if (id) {
const base = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : '';
fetch(base + '/api/sites/' + id + '/click', { method: 'POST', keepalive: true }).catch(() => {});
}
});
}
// 注册 PWA Service Worker
await registerServiceWorker();
// 加载网站数据
await loadSites();
// 搜索输入
document.getElementById('search-input').addEventListener('input', filterSites);
// 网页搜索表单提交
const webSearchForm = document.getElementById('web-search-form');
if (webSearchForm) {
webSearchForm.addEventListener('submit', (e) => {
e.preventDefault();
const query = document.getElementById('web-search-input').value.trim();
const engine = document.getElementById('search-engine').value;
if (query) {
performWebSearch(query, engine);
document.getElementById('web-search-input').value = '';
}
});
}
});