refactor: remove legacy service worker and manifest, integrate Vite PWA plugin
- Deleted the old service worker implementation and manifest file. - Integrated Vite PWA plugin for improved service worker management. - Updated main.js to remove service worker registration logic. - Refactored registerServiceWorker.js to use the new PWA registration method. - Added new service worker (sw.js) with caching strategies for API and static resources. - Updated vite.config.js to include PWA configuration and manifest details.
This commit is contained in:
@@ -30,6 +30,11 @@
|
|||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-vue-devtools": "^8.0.5"
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"workbox-expiration": "^7.4.0",
|
||||||
|
"workbox-precaching": "^7.4.0",
|
||||||
|
"workbox-routing": "^7.4.0",
|
||||||
|
"workbox-strategies": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2803
Web/pnpm-lock.yaml
generated
2803
Web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听跳过等待消息
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
||||||
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] 收到推送消息');
|
|
||||||
let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' };
|
|
||||||
|
|
||||||
if (event.data) {
|
|
||||||
try {
|
|
||||||
const json = event.data.json();
|
|
||||||
data = { ...data, ...json };
|
|
||||||
} catch {
|
|
||||||
data.body = event.data.text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
body: data.body,
|
|
||||||
icon: data.icon,
|
|
||||||
badge: '/icons/icon-72x72.png',
|
|
||||||
vibrate: [200, 100, 200],
|
|
||||||
tag: 'emailbill-notification',
|
|
||||||
requireInteraction: false,
|
|
||||||
data: { url: data.url }
|
|
||||||
};
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(data.title, options)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 通知点击
|
|
||||||
self.addEventListener('notificationclick', (event) => {
|
|
||||||
console.log('[Service Worker] 通知被点击');
|
|
||||||
event.notification.close();
|
|
||||||
const urlToOpen = event.notification.data?.url || '/';
|
|
||||||
event.waitUntil(
|
|
||||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
|
|
||||||
// 如果已经打开了该 URL,则聚焦
|
|
||||||
for (let i = 0; i < windowClients.length; i++) {
|
|
||||||
const client = windowClients[i];
|
|
||||||
if (client.url === urlToOpen && 'focus' in client) {
|
|
||||||
return client.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 否则打开新窗口
|
|
||||||
if (clients.openWindow) {
|
|
||||||
return clients.openWindow(urlToOpen);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 数据同步函数
|
|
||||||
async function syncData() {
|
|
||||||
try {
|
|
||||||
// 这里添加需要同步的逻辑
|
|
||||||
console.log('[Service Worker] 执行数据同步');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Service Worker] 同步失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,9 +10,6 @@ import vant from 'vant'
|
|||||||
import { ConfigProvider } from 'vant';
|
import { ConfigProvider } from 'vant';
|
||||||
import 'vant/lib/index.css'
|
import 'vant/lib/index.css'
|
||||||
|
|
||||||
// 注册 Service Worker
|
|
||||||
import { register } from './registerServiceWorker'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
@@ -22,7 +19,3 @@ app.use(ConfigProvider);
|
|||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
// 在生产环境注册 Service Worker
|
|
||||||
if (import.meta.env.PROD) {
|
|
||||||
register()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,78 +1,40 @@
|
|||||||
import { ref } from 'vue';
|
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||||
|
|
||||||
export const needRefresh = ref(false);
|
const {
|
||||||
let swRegistration = null;
|
needRefresh,
|
||||||
|
updateServiceWorker,
|
||||||
export async function updateServiceWorker() {
|
} = useRegisterSW({
|
||||||
if (swRegistration && swRegistration.waiting) {
|
onRegistered(r) {
|
||||||
await swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
if (r) {
|
||||||
}
|
// 每小时检查一次更新
|
||||||
}
|
|
||||||
|
|
||||||
export function register() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `/service-worker.js`;
|
|
||||||
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then((registration) => {
|
|
||||||
swRegistration = 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] 新版本可用,请刷新页面');
|
|
||||||
needRefresh.value = true;
|
|
||||||
} else {
|
|
||||||
// 首次安装
|
|
||||||
console.log('[SW] 内容已缓存,可离线使用');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 定期检查更新
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
registration.update();
|
r.update();
|
||||||
}, 60 * 60 * 1000); // 每小时检查一次
|
}, 60 * 60 * 1000);
|
||||||
})
|
|
||||||
.catch((error) => {
|
// 当页面重新获得焦点时检查更新
|
||||||
console.error('[SW] Service Worker 注册失败:', error);
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
r.update();
|
||||||
|
console.log('[PWA] 页面进入前台,检查更新');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRegisterError(error) {
|
||||||
|
console.error('[PWA] Service Worker 注册失败:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 Service Worker 控制器变化
|
export { needRefresh, updateServiceWorker };
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
||||||
console.log('[SW] 控制器已更改,页面将刷新');
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
// 兼容老代码的 register 调用
|
||||||
if ('serviceWorker' in navigator) {
|
export function register() {
|
||||||
navigator.serviceWorker.ready
|
console.log('[PWA] 自动注册中...');
|
||||||
.then((registration) => {
|
|
||||||
registration.unregister();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error.message);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
export function requestNotificationPermission() {
|
export function requestNotificationPermission() {
|
||||||
if ('Notification' in window && 'serviceWorker' in navigator) {
|
if ('Notification' in window) {
|
||||||
Notification.requestPermission().then((permission) => {
|
Notification.requestPermission().then((permission) => {
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
console.log('[SW] 通知权限已授予');
|
console.log('[SW] 通知权限已授予');
|
||||||
@@ -81,7 +43,7 @@ export function requestNotificationPermission() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后台同步
|
// 后台同步兼容性导出
|
||||||
export function registerBackgroundSync(tag = 'sync-data') {
|
export function registerBackgroundSync(tag = 'sync-data') {
|
||||||
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
@@ -93,3 +55,4 @@ export function registerBackgroundSync(tag = 'sync-data') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
Web/src/sw.js
Normal file
111
Web/src/sw.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { precacheAndRoute } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
|
||||||
|
// 注入预缓存清单
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// API 请求使用网络优先策略
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.pathname.startsWith('/api/'),
|
||||||
|
new NetworkFirst({
|
||||||
|
cacheName: 'api-cache',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 24 * 60 * 60, // 24 Hours
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 静态资源使用缓存优先策略 (不在预缓存清单中的)
|
||||||
|
registerRoute(
|
||||||
|
({ request }) => request.destination === 'image' || request.destination === 'style' || request.destination === 'script',
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: 'static-resources',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 60,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听跳过等待消息
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 后台同步
|
||||||
|
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] 收到推送消息');
|
||||||
|
let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' };
|
||||||
|
|
||||||
|
if (event.data) {
|
||||||
|
try {
|
||||||
|
const json = event.data.json();
|
||||||
|
data = { ...data, ...json };
|
||||||
|
} catch {
|
||||||
|
data.body = event.data.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
body: data.body,
|
||||||
|
icon: data.icon,
|
||||||
|
badge: '/icons/icon-72x72.png',
|
||||||
|
vibrate: [200, 100, 200],
|
||||||
|
tag: 'emailbill-notification',
|
||||||
|
requireInteraction: false,
|
||||||
|
data: { url: data.url }
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(data.title, options)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通知点击
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
console.log('[Service Worker] 通知被点击');
|
||||||
|
event.notification.close();
|
||||||
|
const urlToOpen = event.notification.data?.url || '/';
|
||||||
|
event.waitUntil(
|
||||||
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
|
||||||
|
// 如果已经打开了该 URL,则聚焦
|
||||||
|
for (let i = 0; i < windowClients.length; i++) {
|
||||||
|
const client = windowClients[i];
|
||||||
|
if (client.url === urlToOpen && 'focus' in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 否则打开新窗口
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(urlToOpen);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据同步函数
|
||||||
|
async function syncData() {
|
||||||
|
try {
|
||||||
|
// 这里添加需要同步的逻辑
|
||||||
|
console.log('[Service Worker] 执行数据同步');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Service Worker] 同步失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,97 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
|
VitePWA({
|
||||||
|
strategies: 'injectManifest',
|
||||||
|
srcDir: 'src',
|
||||||
|
filename: 'sw.js',
|
||||||
|
registerType: 'prompt', // 使用提示模式,以便在 App.vue 中显示刷新按钮
|
||||||
|
injectRegister: 'auto',
|
||||||
|
manifest: {
|
||||||
|
name: '账单',
|
||||||
|
short_name: '账单',
|
||||||
|
description: '个人账单管理与邮件解析',
|
||||||
|
theme_color: '#1989fa',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: '查看账单',
|
||||||
|
short_name: '账单',
|
||||||
|
description: '快速查看账单列表',
|
||||||
|
url: '/',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'icons/icon-96x96.png',
|
||||||
|
sizes: '96x96'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
enabled: true,
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
})
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user