diff --git a/Web/PWA-README.md b/Web/PWA-README.md
new file mode 100644
index 0000000..5f28270
--- /dev/null
+++ b/Web/PWA-README.md
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Web/index.html b/Web/index.html
index c86fb8d..b6e5d01 100644
--- a/Web/index.html
+++ b/Web/index.html
@@ -1,15 +1,37 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
账单管理
diff --git a/Web/public/icons/icon-128x128.svg b/Web/public/icons/icon-128x128.svg
new file mode 100644
index 0000000..44bcb59
--- /dev/null
+++ b/Web/public/icons/icon-128x128.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-144x144.svg b/Web/public/icons/icon-144x144.svg
new file mode 100644
index 0000000..48bc40b
--- /dev/null
+++ b/Web/public/icons/icon-144x144.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-152x152.svg b/Web/public/icons/icon-152x152.svg
new file mode 100644
index 0000000..aa159c7
--- /dev/null
+++ b/Web/public/icons/icon-152x152.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-192x192.svg b/Web/public/icons/icon-192x192.svg
new file mode 100644
index 0000000..61d2062
--- /dev/null
+++ b/Web/public/icons/icon-192x192.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-384x384.svg b/Web/public/icons/icon-384x384.svg
new file mode 100644
index 0000000..f7c72af
--- /dev/null
+++ b/Web/public/icons/icon-384x384.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-512x512.svg b/Web/public/icons/icon-512x512.svg
new file mode 100644
index 0000000..6e773a0
--- /dev/null
+++ b/Web/public/icons/icon-512x512.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-72x72.svg b/Web/public/icons/icon-72x72.svg
new file mode 100644
index 0000000..1ed6c9f
--- /dev/null
+++ b/Web/public/icons/icon-72x72.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-96x96.svg b/Web/public/icons/icon-96x96.svg
new file mode 100644
index 0000000..5b239fc
--- /dev/null
+++ b/Web/public/icons/icon-96x96.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/Web/public/icons/icon-temp.svg b/Web/public/icons/icon-temp.svg
new file mode 100644
index 0000000..2c855ac
--- /dev/null
+++ b/Web/public/icons/icon-temp.svg
@@ -0,0 +1,4 @@
+
diff --git a/Web/public/manifest.json b/Web/public/manifest.json
new file mode 100644
index 0000000..cab70d3
--- /dev/null
+++ b/Web/public/manifest.json
@@ -0,0 +1,76 @@
+{
+ "name": "账单管理系统",
+ "short_name": "账单管理",
+ "description": "个人账单管理与邮件解析系统",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#1989fa",
+ "orientation": "portrait-primary",
+ "icons": [
+ {
+ "src": "/icons/icon-72x72.svg",
+ "sizes": "72x72",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-96x96.svg",
+ "sizes": "96x96",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-128x128.svg",
+ "sizes": "128x128",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-144x144.svg",
+ "sizes": "144x144",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-152x152.svg",
+ "sizes": "152x152",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-192x192.svg",
+ "sizes": "192x192",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-384x384.svg",
+ "sizes": "384x384",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/icons/icon-512x512.svg",
+ "sizes": "512x512",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ }
+ ],
+ "categories": ["finance", "productivity"],
+ "screenshots": [],
+ "shortcuts": [
+ {
+ "name": "查看账单",
+ "short_name": "账单",
+ "description": "快速查看账单列表",
+ "url": "/",
+ "icons": [
+ {
+ "src": "/icons/icon-96x96.png",
+ "sizes": "96x96"
+ }
+ ]
+ }
+ ]
+}
diff --git a/Web/public/service-worker.js b/Web/public/service-worker.js
new file mode 100644
index 0000000..9ec0dc7
--- /dev/null
+++ b/Web/public/service-worker.js
@@ -0,0 +1,141 @@
+const CACHE_NAME = 'emailbill-v1';
+const urlsToCache = [
+ '/',
+ '/index.html',
+ '/favicon.ico',
+ '/manifest.json'
+];
+
+// 安装 Service Worker
+self.addEventListener('install', (event) => {
+ console.log('[Service Worker] 安装中...');
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ console.log('[Service Worker] 缓存文件');
+ return cache.addAll(urlsToCache);
+ })
+ .then(() => self.skipWaiting())
+ );
+});
+
+// 激活 Service Worker
+self.addEventListener('activate', (event) => {
+ console.log('[Service Worker] 激活中...');
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== CACHE_NAME) {
+ console.log('[Service Worker] 删除旧缓存:', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ }).then(() => self.clients.claim())
+ );
+});
+
+// 拦截请求
+self.addEventListener('fetch', (event) => {
+ const { request } = event;
+ const url = new URL(request.url);
+
+ // 跳过跨域请求
+ if (url.origin !== location.origin) {
+ return;
+ }
+
+ // API请求使用网络优先策略
+ if (url.pathname.startsWith('/api/')) {
+ event.respondWith(
+ fetch(request)
+ .then((response) => {
+ // 克隆响应以便缓存
+ const responseClone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(request, responseClone);
+ });
+ return response;
+ })
+ .catch(() => {
+ // 网络失败时尝试从缓存获取
+ return caches.match(request);
+ })
+ );
+ return;
+ }
+
+ // 静态资源使用缓存优先策略
+ event.respondWith(
+ caches.match(request)
+ .then((response) => {
+ if (response) {
+ return response;
+ }
+ return fetch(request).then((response) => {
+ // 检查是否是有效响应
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ return response;
+ }
+
+ const responseClone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(request, responseClone);
+ });
+
+ return response;
+ });
+ })
+ .catch(() => {
+ // 返回离线页面或默认内容
+ if (request.destination === 'document') {
+ return caches.match('/index.html');
+ }
+ })
+ );
+});
+
+// 后台同步
+self.addEventListener('sync', (event) => {
+ console.log('[Service Worker] 后台同步:', event.tag);
+ if (event.tag === 'sync-data') {
+ event.waitUntil(syncData());
+ }
+});
+
+// 推送通知
+self.addEventListener('push', (event) => {
+ console.log('[Service Worker] 收到推送消息');
+ const options = {
+ body: event.data ? event.data.text() : '您有新的账单消息',
+ icon: '/icons/icon-192x192.png',
+ badge: '/icons/icon-72x72.png',
+ vibrate: [200, 100, 200],
+ tag: 'emailbill-notification',
+ requireInteraction: false
+ };
+
+ event.waitUntil(
+ self.registration.showNotification('账单管理', options)
+ );
+});
+
+// 通知点击
+self.addEventListener('notificationclick', (event) => {
+ console.log('[Service Worker] 通知被点击');
+ event.notification.close();
+ event.waitUntil(
+ clients.openWindow('/')
+ );
+});
+
+// 数据同步函数
+async function syncData() {
+ try {
+ // 这里添加需要同步的逻辑
+ console.log('[Service Worker] 执行数据同步');
+ } catch (error) {
+ console.error('[Service Worker] 同步失败:', error);
+ }
+}
diff --git a/Web/src/App.vue b/Web/src/App.vue
index 98fe4e2..be1b934 100644
--- a/Web/src/App.vue
+++ b/Web/src/App.vue
@@ -1,7 +1,7 @@
-
+
日历
@@ -61,3 +61,19 @@ const handleTabClick = (path) => {
+
diff --git a/Web/src/assets/main.css b/Web/src/assets/main.css
index ead0bf6..9491c76 100644
--- a/Web/src/assets/main.css
+++ b/Web/src/assets/main.css
@@ -1,9 +1,35 @@
@import './base.css';
+/* 禁用页面弹性缩放和橡皮筋效果 */
+html, body {
+ overscroll-behavior: none;
+ overscroll-behavior-y: none;
+ -webkit-overflow-scrolling: touch;
+ touch-action: pan-y;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+body {
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+}
+
#app {
max-width: 1280px;
margin: 0 auto;
font-weight: normal;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
}
a,
diff --git a/Web/src/main.js b/Web/src/main.js
index 0d4cf44..6888638 100644
--- a/Web/src/main.js
+++ b/Web/src/main.js
@@ -9,6 +9,9 @@ import vant from 'vant'
import { ConfigProvider } from 'vant';
import 'vant/lib/index.css'
+// 注册 Service Worker
+import { register } from './registerServiceWorker'
+
const app = createApp(App)
app.use(createPinia())
@@ -17,3 +20,8 @@ app.use(vant)
app.use(ConfigProvider);
app.mount('#app')
+
+// 在生产环境注册 Service Worker
+if (import.meta.env.PROD) {
+ register()
+}
diff --git a/Web/src/registerServiceWorker.js b/Web/src/registerServiceWorker.js
new file mode 100644
index 0000000..7c09967
--- /dev/null
+++ b/Web/src/registerServiceWorker.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-console */
+
+export function register() {
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ const swUrl = `/service-worker.js`;
+
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ console.log('[SW] Service Worker 注册成功:', registration.scope);
+
+ // 检查更新
+ registration.addEventListener('updatefound', () => {
+ const newWorker = registration.installing;
+ console.log('[SW] 发现新版本');
+
+ newWorker.addEventListener('statechange', () => {
+ if (newWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // 新的 Service Worker 已安装,提示用户刷新
+ console.log('[SW] 新版本可用,请刷新页面');
+ showUpdateNotification();
+ } else {
+ // 首次安装
+ console.log('[SW] 内容已缓存,可离线使用');
+ }
+ }
+ });
+ });
+
+ // 定期检查更新
+ setInterval(() => {
+ registration.update();
+ }, 60 * 60 * 1000); // 每小时检查一次
+ })
+ .catch((error) => {
+ console.error('[SW] Service Worker 注册失败:', error);
+ });
+
+ // 监听 Service Worker 控制器变化
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
+ console.log('[SW] 控制器已更改,页面将刷新');
+ window.location.reload();
+ });
+ });
+ }
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then((registration) => {
+ registration.unregister();
+ })
+ .catch((error) => {
+ console.error(error.message);
+ });
+ }
+}
+
+// 显示更新提示
+function showUpdateNotification() {
+ // 你可以使用 Vant 的 Dialog 或 Notify 组件
+ if (window.confirm('发现新版本,是否立即更新?')) {
+ window.location.reload();
+ }
+}
+
+// 请求通知权限
+export function requestNotificationPermission() {
+ if ('Notification' in window && 'serviceWorker' in navigator) {
+ Notification.requestPermission().then((permission) => {
+ if (permission === 'granted') {
+ console.log('[SW] 通知权限已授予');
+ }
+ });
+ }
+}
+
+// 后台同步
+export function registerBackgroundSync(tag = 'sync-data') {
+ if ('serviceWorker' in navigator && 'SyncManager' in window) {
+ navigator.serviceWorker.ready.then((registration) => {
+ return registration.sync.register(tag);
+ }).then(() => {
+ console.log('[SW] 后台同步已注册:', tag);
+ }).catch((err) => {
+ console.error('[SW] 后台同步注册失败:', err);
+ });
+ }
+}
diff --git a/Web/src/views/LoginView.vue b/Web/src/views/LoginView.vue
index 1c7cad6..cbb6b7d 100644
--- a/Web/src/views/LoginView.vue
+++ b/Web/src/views/LoginView.vue
@@ -53,7 +53,7 @@ const handleLogin = async () => {
try {
await authStore.login(password.value)
showToast({ type: 'success', message: '登录成功' })
- router.push('/')
+ router.push('/calendar')
} catch (error) {
showToast({ type: 'fail', message: error.message || '登录失败' })
} finally {
diff --git a/Web/vite.config.js b/Web/vite.config.js
index 4217010..6495fef 100644
--- a/Web/vite.config.js
+++ b/Web/vite.config.js
@@ -15,4 +15,18 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
+ build: {
+ // 确保 Service Worker 和 manifest 被正确复制
+ rollupOptions: {
+ input: {
+ main: fileURLToPath(new URL('./index.html', import.meta.url))
+ }
+ }
+ },
+ server: {
+ headers: {
+ // 允许 Service Worker 在开发环境中工作
+ 'Service-Worker-Allowed': '/'
+ }
+ }
})