Compare commits
No commits in common. "73155dbbfb5c21f7594a3cb58b8b51e394609bdf" and "b2199b8902fa610e1087bed3760ffd4454d1c441" have entirely different histories.
73155dbbfb
...
b2199b8902
|
|
@ -0,0 +1,25 @@
|
||||||
|
const CACHE_NAME = "cents-shell-v3";
|
||||||
|
const APP_SHELL = ["/", "/manifest.webmanifest"];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.method !== "GET") return;
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((cached) => {
|
||||||
|
if (cached) return cached;
|
||||||
|
return fetch(event.request).catch(() => caches.match("/"));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,19 +1,4 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeUnmount, onMounted } from "vue";
|
|
||||||
import { useAuthStore } from "./stores/auth";
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
|
||||||
const retrySession = () => {
|
|
||||||
if (typeof navigator === "undefined" || navigator.onLine) void auth.loadSession(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => window.addEventListener("online", retrySession));
|
|
||||||
onBeforeUnmount(() => window.removeEventListener("online", retrySession));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<div v-if="auth.unavailable && auth.user" class="offline-status" role="status">
|
|
||||||
服务暂不可用,当前显示本地数据
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,6 @@ import { closeUserDb } from "../data/db";
|
||||||
export type SessionUser = { id: string; name: string };
|
export type SessionUser = { id: string; name: string };
|
||||||
|
|
||||||
let sessionPromise: Promise<void> | null = null;
|
let sessionPromise: Promise<void> | null = null;
|
||||||
const LAST_SESSION_KEY = "cents:last-session-user";
|
|
||||||
|
|
||||||
function readLastSession(): SessionUser | null {
|
|
||||||
try {
|
|
||||||
const value = localStorage.getItem(LAST_SESSION_KEY);
|
|
||||||
if (!value) return null;
|
|
||||||
const user = JSON.parse(value) as SessionUser;
|
|
||||||
return user?.id && user?.name ? user : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeLastSession(user: SessionUser | null) {
|
|
||||||
try {
|
|
||||||
if (user) localStorage.setItem(LAST_SESSION_KEY, JSON.stringify(user));
|
|
||||||
else localStorage.removeItem(LAST_SESSION_KEY);
|
|
||||||
} catch {
|
|
||||||
// Storage can be disabled by the browser; server auth still works.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", {
|
export const useAuthStore = defineStore("auth", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|
@ -42,9 +21,8 @@ export const useAuthStore = defineStore("auth", {
|
||||||
const result = await apiRequest<{ user: SessionUser | null }>("/api/auth/session");
|
const result = await apiRequest<{ user: SessionUser | null }>("/api/auth/session");
|
||||||
this.user = result.user;
|
this.user = result.user;
|
||||||
this.unavailable = false;
|
this.unavailable = false;
|
||||||
writeLastSession(result.user);
|
|
||||||
} catch {
|
} catch {
|
||||||
this.user = readLastSession();
|
this.user = null;
|
||||||
this.unavailable = true;
|
this.unavailable = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
@ -62,7 +40,6 @@ export const useAuthStore = defineStore("auth", {
|
||||||
body: { name },
|
body: { name },
|
||||||
});
|
});
|
||||||
this.user = result.user;
|
this.user = result.user;
|
||||||
writeLastSession(result.user);
|
|
||||||
},
|
},
|
||||||
async login(name: string, password: string) {
|
async login(name: string, password: string) {
|
||||||
const result = await apiRequest<{ user: SessionUser }>("/api/auth/login", {
|
const result = await apiRequest<{ user: SessionUser }>("/api/auth/login", {
|
||||||
|
|
@ -72,7 +49,6 @@ export const useAuthStore = defineStore("auth", {
|
||||||
this.user = result.user;
|
this.user = result.user;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.unavailable = false;
|
this.unavailable = false;
|
||||||
writeLastSession(result.user);
|
|
||||||
},
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
const userId = this.user?.id;
|
const userId = this.user?.id;
|
||||||
|
|
@ -80,7 +56,6 @@ export const useAuthStore = defineStore("auth", {
|
||||||
if (userId) closeUserDb(userId);
|
if (userId) closeUserDb(userId);
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
writeLastSession(null);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,7 @@ export const useLedgerStore = defineStore("ledgers", {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
if (!auth.user) return;
|
if (!auth.user) return;
|
||||||
const db = await getUserDb(auth.user.id);
|
const db = await getUserDb(auth.user.id);
|
||||||
let serverLedgers: LedgerRecord[];
|
const result = await apiRequest<{ ledgers: LedgerRecord[] }>("/api/ledgers");
|
||||||
try {
|
|
||||||
const result = await apiRequest<{ ledgers: LedgerRecord[] }>("/api/ledgers");
|
|
||||||
serverLedgers = result.ledgers;
|
|
||||||
} catch (error) {
|
|
||||||
const cachedLedgers = await db.ledgers.toArray();
|
|
||||||
if (!cachedLedgers.length) throw error;
|
|
||||||
serverLedgers = cachedLedgers;
|
|
||||||
}
|
|
||||||
const displayPreferences = await db.userPreferences
|
const displayPreferences = await db.userPreferences
|
||||||
.where("userId")
|
.where("userId")
|
||||||
.equals(auth.user.id)
|
.equals(auth.user.id)
|
||||||
|
|
@ -65,16 +57,17 @@ export const useLedgerStore = defineStore("ledgers", {
|
||||||
const summaryByLedger = new Map(
|
const summaryByLedger = new Map(
|
||||||
summaryPreferences.map((preference) => [preference.id.split(":").at(-1), preference.value as LedgerSummaryRange]),
|
summaryPreferences.map((preference) => [preference.id.split(":").at(-1), preference.value as LedgerSummaryRange]),
|
||||||
);
|
);
|
||||||
const hydratedLedgers = serverLedgers.map((ledger) => ({
|
const serverLedgers = result.ledgers.map((ledger) => ({
|
||||||
...ledger,
|
...ledger,
|
||||||
amountDisplay: displayByLedger.get(ledger.id) ?? "base",
|
amountDisplay: displayByLedger.get(ledger.id) ?? "base",
|
||||||
summaryRange: summaryByLedger.get(ledger.id) ?? "year",
|
summaryRange: summaryByLedger.get(ledger.id) ?? "year",
|
||||||
}));
|
}));
|
||||||
this.ledgers = hydratedLedgers;
|
this.ledgers = serverLedgers;
|
||||||
|
|
||||||
|
const allowedIds = new Set(this.ledgers.map((ledger) => ledger.id));
|
||||||
await db.transaction("rw", db.ledgers, async () => {
|
await db.transaction("rw", db.ledgers, async () => {
|
||||||
await db.ledgers.clear();
|
await db.ledgers.clear();
|
||||||
if (hydratedLedgers.length) await db.ledgers.bulkPut(hydratedLedgers);
|
if (serverLedgers.length) await db.ledgers.bulkPut(serverLedgers);
|
||||||
});
|
});
|
||||||
|
|
||||||
const preferenceId = currentLedgerPreferenceId(auth.user.id);
|
const preferenceId = currentLedgerPreferenceId(auth.user.id);
|
||||||
|
|
@ -83,11 +76,7 @@ export const useLedgerStore = defineStore("ledgers", {
|
||||||
this.currentLedgerId = preferredLedger?.id ?? this.ledgers[0]?.id ?? "";
|
this.currentLedgerId = preferredLedger?.id ?? this.ledgers[0]?.id ?? "";
|
||||||
if (this.currentLedgerId !== preference?.value) await this.persistCurrentLedger();
|
if (this.currentLedgerId !== preference?.value) await this.persistCurrentLedger();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
try {
|
await this.loadCurrentMembers();
|
||||||
await this.loadCurrentMembers();
|
|
||||||
} catch {
|
|
||||||
this.currentMembers = [];
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
try {
|
try {
|
||||||
await ledgerLoadPromise;
|
await ledgerLoadPromise;
|
||||||
|
|
@ -127,14 +116,10 @@ export const useLedgerStore = defineStore("ledgers", {
|
||||||
this.currentMembers = [];
|
this.currentMembers = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const result = await apiRequest<{ members: LedgerMember[] }>(
|
||||||
const result = await apiRequest<{ members: LedgerMember[] }>(
|
`/api/ledgers/${encodeURIComponent(this.currentLedgerId)}/members`,
|
||||||
`/api/ledgers/${encodeURIComponent(this.currentLedgerId)}/members`,
|
);
|
||||||
);
|
this.currentMembers = result.members;
|
||||||
this.currentMembers = result.members;
|
|
||||||
} catch {
|
|
||||||
this.currentMembers = [];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
memberName(userId: string) {
|
memberName(userId: string) {
|
||||||
return this.currentMembers.find((member) => member.id === userId)?.name ?? "历史用户";
|
return this.currentMembers.find((member) => member.id === userId)?.name ?? "历史用户";
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,6 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offline-status {
|
|
||||||
position: fixed;
|
|
||||||
top: max(10px, env(safe-area-inset-top));
|
|
||||||
left: 50%;
|
|
||||||
z-index: 100;
|
|
||||||
max-width: calc(100% - 32px);
|
|
||||||
padding: 7px 12px;
|
|
||||||
border: 1px solid #e9c98d;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 248, 228, .96);
|
|
||||||
color: #795b19;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.2;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
|
|
|
||||||
|
|
@ -81,75 +81,9 @@ function appIdentityPlugin(mode: string): Plugin {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function offlineServiceWorkerPlugin(): Plugin {
|
|
||||||
const publicAssets = [
|
|
||||||
"/apple-touch-icon.png",
|
|
||||||
"/apple-touch-icon-dev.png",
|
|
||||||
"/icon-192.png",
|
|
||||||
"/icon-512.png",
|
|
||||||
"/icon-maskable-512.png",
|
|
||||||
"/icon-dev-192.png",
|
|
||||||
"/icon-dev-512.png",
|
|
||||||
"/icon-dev-maskable-512.png",
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "cents-offline-service-worker",
|
|
||||||
generateBundle(_options, bundle) {
|
|
||||||
const bundledAssets = Object.keys(bundle)
|
|
||||||
.filter((fileName) => fileName !== "sw.js")
|
|
||||||
.map((fileName) => `/${fileName}`);
|
|
||||||
const precache = [...new Set([...bundledAssets, ...publicAssets])];
|
|
||||||
const source = `const CACHE_NAME = "cents-shell-v4";
|
|
||||||
const PRECACHE = ${JSON.stringify(precache, null, 2)};
|
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
|
||||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()));
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
|
||||||
event.waitUntil(
|
|
||||||
caches.keys().then((keys) => Promise.all(
|
|
||||||
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)),
|
|
||||||
)).then(() => self.clients.claim()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
|
||||||
if (event.request.method !== "GET") return;
|
|
||||||
const url = new URL(event.request.url);
|
|
||||||
if (url.origin !== self.location.origin || url.pathname.startsWith("/api/")) return;
|
|
||||||
|
|
||||||
if (event.request.mode === "navigate") {
|
|
||||||
event.respondWith(
|
|
||||||
fetch(event.request).then((response) => {
|
|
||||||
const copy = response.clone();
|
|
||||||
void caches.open(CACHE_NAME).then((cache) => cache.put("/", copy));
|
|
||||||
return response;
|
|
||||||
}).catch(() => caches.match("/")),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(event.request).then((cached) => cached ?? fetch(event.request).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
const copy = response.clone();
|
|
||||||
void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy));
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
this.emitFile({ type: "asset", fileName: "sw.js", source });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
envDir: monorepoRoot,
|
envDir: monorepoRoot,
|
||||||
plugins: [vue(), appIdentityPlugin(mode), offlineServiceWorkerPlugin()],
|
plugins: [vue(), appIdentityPlugin(mode)],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ["dev.cents.homemade.net.cn"],
|
allowedHosts: ["dev.cents.homemade.net.cn"],
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue