Files
timeline-frontend/public/sw.js
jianghao 97a5ad3a00
Some checks failed
test/timeline-frontend/pipeline/head Something is wrong with the build of this commit
feat: 实现时间线拖拽排序功能及PWA支持
新增时间线节点的拖拽排序功能,使用dnd-kit库实现可排序网格布局。添加PWA支持,包括Service Worker注册和manifest配置。优化移动端适配,改进批量操作工具栏和撤销/重做功能。

重构用户登录和注册页面,修复登录跳转逻辑。调整画廊视图在不同设备上的显示效果。新增协作成员管理功能,支持批量修改权限。

修复请求错误处理中的跳转逻辑问题,避免重复跳转登录页。优化样式表,增强时间线卡片和图片展示的响应式布局。

新增多个API接口支持批量操作,包括排序、删除和时间修改。引入useBatchSelection和useHistory自定义Hook管理状态。添加UndoRedoToolbar组件提供撤销/重做功能。

实现Service Worker离线缓存策略,支持静态资源和API请求的缓存。新增PWA工具函数处理安装提示和更新检测。优化移动端交互,调整组件布局和操作按钮。
2026-02-24 10:33:10 +08:00

248 lines
5.7 KiB
JavaScript
Raw 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.
/**
* Service Worker - PWA 离线缓存
*
* 功能描述:
* 提供 PWA 离线访问能力,缓存关键资源。
*
* 缓存策略:
* - 静态资源Cache First
* - API 请求Network First
* - 图片资源Stale While Revalidate
*
* @author Timeline Team
* @date 2024
*/
const CACHE_NAME = 'timeline-v1';
const STATIC_CACHE = 'timeline-static-v1';
const API_CACHE = 'timeline-api-v1';
const IMAGE_CACHE = 'timeline-image-v1';
// 需要缓存的静态资源
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png',
];
// 安装事件
self.addEventListener('install', (event) => {
console.log('[ServiceWorker] 安装');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[ServiceWorker] 缓存静态资源');
return cache.addAll(STATIC_ASSETS);
})
);
// 立即激活
self.skipWaiting();
});
// 激活事件
self.addEventListener('activate', (event) => {
console.log('[ServiceWorker] 激活');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 删除旧版本缓存
if (cacheName !== STATIC_CACHE &&
cacheName !== API_CACHE &&
cacheName !== IMAGE_CACHE) {
console.log('[ServiceWorker] 删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// 立即控制所有页面
self.clients.claim();
});
// 请求拦截
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 只处理同源请求
if (url.origin !== location.origin) {
return;
}
// 根据请求类型选择缓存策略
if (isApiRequest(url)) {
// API 请求Network First
event.respondWith(networkFirst(request, API_CACHE));
} else if (isImageRequest(request)) {
// 图片请求Stale While Revalidate
event.respondWith(staleWhileRevalidate(request, IMAGE_CACHE));
} else {
// 静态资源Cache First
event.respondWith(cacheFirst(request, STATIC_CACHE));
}
});
/**
* 判断是否为 API 请求
*/
function isApiRequest(url) {
return url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/story/') ||
url.pathname.startsWith('/file/') ||
url.pathname.startsWith('/user/');
}
/**
* 判断是否为图片请求
*/
function isImageRequest(request) {
return request.destination === 'image' ||
request.url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i);
}
/**
* Cache First 策略
* 优先使用缓存,缓存不存在时请求网络
*/
async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
return cached;
}
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
// 网络请求失败,返回离线页面
if (request.mode === 'navigate') {
return caches.match('/index.html');
}
throw error;
}
}
/**
* Network First 策略
* 优先请求网络,失败时使用缓存
*/
async function networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await cache.match(request);
if (cached) {
return cached;
}
throw error;
}
}
/**
* Stale While Revalidate 策略
* 立即返回缓存,同时后台更新
*/
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
// 后台更新缓存
const fetchPromise = fetch(request).then((response) => {
if (response.ok) {
cache.put(request, response.clone());
}
return response;
});
// 返回缓存或等待网络响应
return cached || fetchPromise;
}
// 推送通知
self.addEventListener('push', (event) => {
console.log('[ServiceWorker] 收到推送');
const data = event.data ? event.data.json() : {};
const options = {
body: data.body || '您有新的消息',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
url: data.url || '/'
},
actions: [
{ action: 'open', title: '查看' },
{ action: 'close', title: '关闭' }
]
};
event.waitUntil(
self.registration.showNotification(data.title || 'Timeline', options)
);
});
// 通知点击
self.addEventListener('notificationclick', (event) => {
console.log('[ServiceWorker] 通知点击');
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
// 后台同步
self.addEventListener('sync', (event) => {
console.log('[ServiceWorker] 后台同步:', event.tag);
if (event.tag === 'sync-timeline') {
event.waitUntil(syncTimeline());
}
});
/**
* 同步时间线数据
*/
async function syncTimeline() {
// 获取待同步的数据
const pendingData = await getPendingData();
for (const item of pendingData) {
try {
await fetch('/api/story/item', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
// 同步成功,删除待同步记录
await removePendingData(item.id);
} catch (error) {
console.error('[ServiceWorker] 同步失败:', error);
}
}
}