499 lines
16 KiB
JavaScript
499 lines
16 KiB
JavaScript
// 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 = '';
|
||
}
|
||
});
|
||
}
|
||
});
|