初始化
Some checks failed
CI/CD Pipeline / 测试 (18.x) (push) Has been cancelled
CI/CD Pipeline / 测试 (20.x) (push) Has been cancelled
CI/CD Pipeline / 安全检查 (push) Has been cancelled
CI/CD Pipeline / 部署 (push) Has been cancelled
CI/CD Pipeline / 通知 (push) Has been cancelled

This commit is contained in:
2025-11-03 19:47:36 +08:00
parent 7a04b85667
commit f25b0307db
454 changed files with 37064 additions and 4544 deletions

View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA图标生成器</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.icon-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.icon-item {
text-align: center;
}
.icon-item canvas {
border: 1px solid #ddd;
}
button {
background-color: #409EFF;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
margin-top: 20px;
}
button:hover {
background-color: #3a8ee6;
}
</style>
</head>
<body>
<h1>PWA图标生成器</h1>
<p>此页面用于生成PWA所需的不同尺寸图标。右键点击图标并选择"另存为"来下载。</p>
<button id="generateIcons">生成图标</button>
<div class="icon-container" id="iconContainer"></div>
<script>
document.getElementById('generateIcons').addEventListener('click', function() {
const container = document.getElementById('iconContainer');
container.innerHTML = '';
// 需要生成的图标尺寸
const sizes = [
{ size: 72, name: 'icon-72x72.png' },
{ size: 96, name: 'icon-96x96.png' },
{ size: 128, name: 'icon-128x128.png' },
{ size: 144, name: 'icon-144x144.png' },
{ size: 152, name: 'icon-152x152.png' },
{ size: 192, name: 'icon-192x192.png' },
{ size: 384, name: 'icon-384x384.png' },
{ size: 512, name: 'icon-512x512.png' }
];
sizes.forEach(item => {
const canvas = document.createElement('canvas');
canvas.width = item.size;
canvas.height = item.size;
const ctx = canvas.getContext('2d');
// 绘制背景
ctx.fillStyle = '#409EFF';
ctx.fillRect(0, 0, item.size, item.size);
// 绘制圆角
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
ctx.roundRect(0, 0, item.size, item.size, item.size * 0.125);
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
// 绘制图标
const center = item.size / 2;
const outerRadius = center * 0.75;
const innerRadius = center * 0.5;
const dotRadius = center * 0.125;
// 外圆
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(center, center, outerRadius, 0, 2 * Math.PI);
ctx.fill();
// 内圆
ctx.fillStyle = '#409EFF';
ctx.beginPath();
ctx.arc(center, center, innerRadius, 0, 2 * Math.PI);
ctx.fill();
// 中心点
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(center, center, dotRadius, 0, 2 * Math.PI);
ctx.fill();
// 创建容器
const iconItem = document.createElement('div');
iconItem.className = 'icon-item';
// 添加标题
const title = document.createElement('div');
title.textContent = `${item.name} (${item.size}x${item.size})`;
iconItem.appendChild(title);
// 添加画布
iconItem.appendChild(canvas);
// 添加下载链接
const downloadLink = document.createElement('a');
downloadLink.href = canvas.toDataURL('image/png');
downloadLink.download = item.name;
downloadLink.textContent = '下载';
downloadLink.style.display = 'block';
downloadLink.style.marginTop = '5px';
iconItem.appendChild(downloadLink);
container.appendChild(iconItem);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,6 @@
<svg width="152" height="152" viewBox="0 0 152 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="152" height="152" rx="19" fill="#409EFF"/>
<path d="M76 38C55 38 38 55 38 76C38 97 55 114 76 114C97 114 114 97 114 76C114 55 97 38 76 38ZM76 104C60.5 104 48 91.5 48 76C48 60.5 60.5 48 76 48C91.5 48 104 60.5 104 76C104 91.5 91.5 104 76 104Z" fill="white"/>
<path d="M76 57C65.5 57 57 65.5 57 76C57 86.5 65.5 95 76 95C86.5 95 95 86.5 95 76C95 65.5 86.5 57 76 57ZM76 85C71.6 85 68 81.4 68 77C68 72.6 71.6 69 76 69C80.4 69 84 72.6 84 77C84 81.4 80.4 85 76 85Z" fill="white"/>
<circle cx="76" cy="76" r="9" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -0,0 +1,6 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="192" rx="24" fill="#409EFF"/>
<path d="M96 48C69.5 48 48 69.5 48 96C48 122.5 69.5 144 96 144C122.5 144 144 122.5 144 96C144 69.5 122.5 48 96 48ZM96 132C75.6 132 60 116.4 60 96C60 75.6 75.6 60 96 60C116.4 60 132 75.6 132 96C132 116.4 116.4 132 96 132Z" fill="white"/>
<path d="M96 72C82.7 72 72 82.7 72 96C72 109.3 82.7 120 96 120C109.3 120 120 109.3 120 96C120 82.7 109.3 72 96 72ZM96 108C89.4 108 84 102.6 84 96C84 89.4 89.4 84 96 84C102.6 84 108 89.4 108 96C108 102.6 102.6 108 96 108Z" fill="white"/>
<circle cx="96" cy="96" r="12" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 690 B

View File

@@ -0,0 +1,142 @@
{
"name": "硬件性能排行榜",
"short_name": "硬件排行",
"description": "专业的硬件性能排行榜应用提供CPU、GPU等硬件性能数据和对比功能",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#409EFF",
"orientation": "portrait-primary",
"scope": "/",
"lang": "zh-CN",
"categories": ["utilities", "productivity", "reference"],
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"screenshots": [
{
"src": "/screenshots/home-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "首页 - 桌面版"
},
{
"src": "/screenshots/home-mobile.png",
"sizes": "375x667",
"type": "image/png",
"form_factor": "narrow",
"label": "首页 - 移动版"
},
{
"src": "/screenshots/ranking-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "排行榜 - 桌面版"
},
{
"src": "/screenshots/ranking-mobile.png",
"sizes": "375x667",
"type": "image/png",
"form_factor": "narrow",
"label": "排行榜 - 移动版"
}
],
"shortcuts": [
{
"name": "CPU排行榜",
"short_name": "CPU排行",
"description": "查看CPU性能排行榜",
"url": "/category/1",
"icons": [
{
"src": "/icons/shortcut-cpu.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "GPU排行榜",
"short_name": "GPU排行",
"description": "查看GPU性能排行榜",
"url": "/category/2",
"icons": [
{
"src": "/icons/shortcut-gpu.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "产品对比",
"short_name": "对比",
"description": "对比硬件产品性能",
"url": "/comparison",
"icons": [
{
"src": "/icons/shortcut-compare.png",
"sizes": "96x96",
"type": "image/png"
}
]
}
],
"related_applications": [],
"prefer_related_applications": false,
"edge_side_panel": {
"preferred_width": 400
},
"launch_handler": {
"client_mode": "focus-existing"
}
}

287
frontend/public/sw.js Normal file
View File

@@ -0,0 +1,287 @@
// Service Worker for PWA functionality
const CACHE_NAME = 'it-hardware-ranking-v1'
const RUNTIME_CACHE = 'it-hardware-ranking-runtime'
// 需要预缓存的资源列表
const STATIC_CACHE_URLS = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
// 添加其他需要预缓存的静态资源
]
// 需要网络优先的资源
const NETWORK_FIRST_URLS = [
'/api/',
// 添加其他需要网络优先的API路径
]
// 需要缓存优先的资源
const CACHE_FIRST_URLS = [
'/static/',
'/assets/',
// 添加其他需要缓存优先的静态资源路径
]
// 安装事件 - 预缓存静态资源
self.addEventListener('install', (event) => {
console.log('[SW] Install event triggered')
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('[SW] Caching static resources')
return cache.addAll(STATIC_CACHE_URLS)
})
.then(() => {
console.log('[SW] Static resources cached successfully')
// 强制激活新的Service Worker
return self.skipWaiting()
})
.catch((error) => {
console.error('[SW] Failed to cache static resources:', error)
})
)
})
// 激活事件 - 清理旧缓存
self.addEventListener('activate', (event) => {
console.log('[SW] Activate event triggered')
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 删除旧版本的缓存
if (cacheName !== CACHE_NAME && cacheName !== RUNTIME_CACHE) {
console.log('[SW] Deleting old cache:', cacheName)
return caches.delete(cacheName)
}
})
)
})
.then(() => {
console.log('[SW] Old caches cleaned up')
// 立即控制所有客户端
return self.clients.claim()
})
.catch((error) => {
console.error('[SW] Failed to clean up old caches:', error)
})
)
})
// 网络请求拦截
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// 跳过非HTTP(S)请求
if (!url.protocol.startsWith('http')) {
return
}
// 跳过Chrome扩展请求
if (url.protocol === 'chrome-extension:') {
return
}
// 根据请求URL选择缓存策略
if (isNetworkFirst(url)) {
// 网络优先策略
event.respondWith(networkFirst(request))
} else if (isCacheFirst(url)) {
// 缓存优先策略
event.respondWith(cacheFirst(request))
} else {
// 缓存优先,网络作为后备策略
event.respondWith(staleWhileRevalidate(request))
}
})
// 判断是否使用网络优先策略
function isNetworkFirst(url) {
return NETWORK_FIRST_URLS.some(path => url.pathname.startsWith(path))
}
// 判断是否使用缓存优先策略
function isCacheFirst(url) {
return CACHE_FIRST_URLS.some(path => url.pathname.startsWith(path))
}
// 网络优先策略
async function networkFirst(request) {
const cache = await caches.open(RUNTIME_CACHE)
try {
// 尝试从网络获取
const response = await fetch(request)
// 如果响应成功,缓存它
if (response.ok) {
cache.put(request, response.clone())
}
return response
} catch (error) {
console.log('[SW] Network request failed, trying cache:', error)
// 网络失败,尝试从缓存获取
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
// 如果缓存也没有,返回离线页面
return new Response('离线状态,请检查网络连接', {
status: 503,
statusText: 'Service Unavailable'
})
}
}
// 缓存优先策略
async function cacheFirst(request) {
const cache = await caches.open(RUNTIME_CACHE)
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
try {
// 缓存中没有,从网络获取
const response = await fetch(request)
// 如果响应成功,缓存它
if (response.ok) {
cache.put(request, response.clone())
}
return response
} catch (error) {
console.log('[SW] Network request failed:', error)
// 返回错误响应
return new Response('网络请求失败', {
status: 500,
statusText: 'Internal Server Error'
})
}
}
// 缓存优先,网络作为后备策略
async function staleWhileRevalidate(request) {
const cache = await caches.open(RUNTIME_CACHE)
const cachedResponse = await cache.match(request)
// 在后台发起网络请求
const fetchPromise = fetch(request).then((response) => {
// 如果响应成功,更新缓存
if (response.ok) {
cache.put(request, response.clone())
}
return response
}).catch((error) => {
console.log('[SW] Background fetch failed:', error)
// 返回错误响应,但不影响缓存的响应
return new Response('网络请求失败', {
status: 500,
statusText: 'Internal Server Error'
})
})
// 如果有缓存,立即返回缓存
if (cachedResponse) {
return cachedResponse
}
// 没有缓存,等待网络请求
return fetchPromise
}
// 后台同步事件
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync event:', event.tag)
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync())
}
})
// 执行后台同步
async function doBackgroundSync() {
try {
// 这里可以执行需要在网络恢复时同步的任务
console.log('[SW] Performing background sync')
// 例如:同步离线时的数据
// await syncOfflineData()
} catch (error) {
console.error('[SW] Background sync failed:', error)
}
}
// 推送通知事件
self.addEventListener('push', (event) => {
console.log('[SW] Push event received')
if (!event.data) {
return
}
const options = event.data.json()
event.waitUntil(
self.registration.showNotification(options.title || '新消息', {
body: options.body || '您有新消息',
icon: options.icon || '/favicon.ico',
badge: options.badge || '/favicon.ico',
data: options.data || {},
actions: options.actions || []
})
)
})
// 通知点击事件
self.addEventListener('notificationclick', (event) => {
console.log('[SW] Notification click event')
event.notification.close()
// 处理通知点击
if (event.action) {
// 处理特定的操作按钮点击
handleNotificationAction(event.action, event.notification.data)
} else {
// 处理通知主体点击
handleNotificationClick(event.notification.data)
}
})
// 处理通知点击
function handleNotificationClick(data) {
// 打开应用或特定页面
event.waitUntil(
clients.openWindow(data.url || '/')
)
}
// 处理通知操作
function handleNotificationAction(action, data) {
// 根据不同的操作执行不同的逻辑
switch (action) {
case 'view':
clients.openWindow(data.url || '/')
break
case 'dismiss':
// 关闭通知,无需其他操作
break
default:
console.log('[SW] Unknown notification action:', action)
}
}