Files
2026-03-11 20:46:24 +08:00

499 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = '';
}
});
}
});