From a5846a7161e9ee61120ba192adbba101e3d188c7 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 5 Jun 2026 10:00:45 +0800 Subject: [PATCH] Add floating translate panel and harden production frontend updates Move translation to a left-side FAB with compact inline input, remove redundant nav and dashboard entry points, and improve deploy cache headers plus stale chunk recovery on login flows. Co-authored-by: Cursor --- deploy/wordloop-locations.inc | 15 +- frontend/src/components/TranslateFab.vue | 310 ++++++++++++++++++++ frontend/src/composables/useTranslateFab.ts | 19 ++ frontend/src/layouts/MainLayout.vue | 24 +- frontend/src/main.ts | 45 ++- frontend/src/pages/Dashboard.vue | 1 - frontend/src/pages/Login.vue | 7 +- frontend/src/pages/Register.vue | 12 +- frontend/tsconfig.tsbuildinfo | 2 +- 9 files changed, 416 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/TranslateFab.vue create mode 100644 frontend/src/composables/useTranslateFab.ts diff --git a/deploy/wordloop-locations.inc b/deploy/wordloop-locations.inc index 719e2ef..639fae0 100644 --- a/deploy/wordloop-locations.inc +++ b/deploy/wordloop-locations.inc @@ -23,15 +23,26 @@ location /openapi.json { proxy_set_header Host $host; } +# 入口 HTML 禁止缓存(避免部署后仍引用已删除的旧 chunk) +location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always; + add_header CDN-Cache-Control "no-store" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; +} + # 静态资源不存在时必须 404,不能回退 index.html(否则 JS 加载失败页面空白) location ^~ /assets/ { try_files $uri =404; expires 7d; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, max-age=604800, immutable" always; } # Vue Router history:/login 等路径回退 index.html location / { try_files $uri $uri/ /index.html; - add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always; + add_header CDN-Cache-Control "no-store" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; } diff --git a/frontend/src/components/TranslateFab.vue b/frontend/src/components/TranslateFab.vue new file mode 100644 index 0000000..48b2e84 --- /dev/null +++ b/frontend/src/components/TranslateFab.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/frontend/src/composables/useTranslateFab.ts b/frontend/src/composables/useTranslateFab.ts new file mode 100644 index 0000000..e284707 --- /dev/null +++ b/frontend/src/composables/useTranslateFab.ts @@ -0,0 +1,19 @@ +import { ref } from 'vue' + +const open = ref(false) + +export function useTranslateFab() { + function openFab() { + open.value = true + } + + function closeFab() { + open.value = false + } + + function toggleFab() { + open.value = !open.value + } + + return { open, openFab, closeFab, toggleFab } +} diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 417cc7d..18b3c33 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -1,16 +1,13 @@ diff --git a/frontend/src/main.ts b/frontend/src/main.ts index cd69284..271127e 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,4 +3,47 @@ import App from './App.vue' import router from './router' import './styles.css' -createApp(App).use(router).mount('#app') +const CHUNK_RELOAD_KEY = 'wordloop-chunk-reload' + +/** 部署后旧 bundle 引用已删除的 chunk 时,强制拉取最新 index.html */ +function reloadOnStaleChunk(): void { + const attempts = Number(sessionStorage.getItem(CHUNK_RELOAD_KEY) || '0') + if (attempts >= 2) return + sessionStorage.setItem(CHUNK_RELOAD_KEY, String(attempts + 1)) + const url = new URL(window.location.href) + url.searchParams.set('_cb', String(Date.now())) + window.location.replace(url.toString()) +} + +function isStaleChunkError(reason: unknown): boolean { + const msg = String( + reason instanceof Error ? reason.message : reason ?? '', + ) + return ( + msg.includes('Failed to fetch dynamically imported module') || + msg.includes('Loading chunk') || + msg.includes('ChunkLoadError') + ) +} + +window.addEventListener('vite:preloadError', (event) => { + event.preventDefault() + reloadOnStaleChunk() +}) + +window.addEventListener('unhandledrejection', (event) => { + if (!isStaleChunkError(event.reason)) return + event.preventDefault() + reloadOnStaleChunk() +}) + +router.onError((error) => { + if (!isStaleChunkError(error)) return + reloadOnStaleChunk() +}) + +const app = createApp(App).use(router) +app.mount('#app') +router.isReady().then(() => { + sessionStorage.removeItem(CHUNK_RELOAD_KEY) +}) diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue index e2c7271..af32619 100644 --- a/frontend/src/pages/Dashboard.vue +++ b/frontend/src/pages/Dashboard.vue @@ -58,7 +58,6 @@ onMounted(async () => {
- 去翻译 单词库 开始每日训练 拼写练习 diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 6a20d66..3582a92 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -41,7 +41,7 @@ async function handleLogin() {

单词循环记忆系统

{{ error }}
-
+
@@ -51,16 +51,15 @@ async function handleLogin() { class="input" placeholder="请输入密码" autocomplete="current-password" - @keydown.enter="handleLogin" /> - -
+
diff --git a/frontend/src/pages/Register.vue b/frontend/src/pages/Register.vue index b8f58a8..cb22862 100644 --- a/frontend/src/pages/Register.vue +++ b/frontend/src/pages/Register.vue @@ -44,17 +44,17 @@ async function handleRegister() {
{{ error }}
-
+
- + - + - - -
+
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index b9276e7..61f5e71 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/request.ts","./src/composables/usegraphfilter.ts","./src/composables/usequizsession.ts","./src/composables/usequiztimer.ts","./src/router/index.ts","./src/utils/auth.ts","./src/utils/buildpracticequestion.ts","./src/utils/practicefocus.ts","./src/utils/practicegraphdisplay.ts","./src/utils/practicegraphsession.ts","./src/utils/spellquestion.ts","./src/app.vue","./src/components/memorycurvechart.vue","./src/components/quizcard.vue","./src/components/spellcard.vue","./src/components/trainfab.vue","./src/components/wordcard.vue","./src/components/wordgraphcanvas.vue","./src/layouts/mainlayout.vue","./src/pages/dailyquiz.vue","./src/pages/dashboard.vue","./src/pages/graphpractice.vue","./src/pages/login.vue","./src/pages/memorycoach.vue","./src/pages/passwordsettings.vue","./src/pages/register.vue","./src/pages/settings.vue","./src/pages/spellquiz.vue","./src/pages/translate.vue","./src/pages/usernamesettings.vue","./src/pages/wordlibrary.vue"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/request.ts","./src/composables/usegraphfilter.ts","./src/composables/usequizsession.ts","./src/composables/usequiztimer.ts","./src/composables/usetranslatefab.ts","./src/router/index.ts","./src/utils/auth.ts","./src/utils/buildpracticequestion.ts","./src/utils/practicefocus.ts","./src/utils/practicegraphdisplay.ts","./src/utils/practicegraphsession.ts","./src/utils/spellquestion.ts","./src/app.vue","./src/components/memorycurvechart.vue","./src/components/quizcard.vue","./src/components/spellcard.vue","./src/components/trainfab.vue","./src/components/translatefab.vue","./src/components/wordcard.vue","./src/components/wordgraphcanvas.vue","./src/layouts/mainlayout.vue","./src/pages/dailyquiz.vue","./src/pages/dashboard.vue","./src/pages/graphpractice.vue","./src/pages/login.vue","./src/pages/memorycoach.vue","./src/pages/passwordsettings.vue","./src/pages/register.vue","./src/pages/settings.vue","./src/pages/spellquiz.vue","./src/pages/translate.vue","./src/pages/usernamesettings.vue","./src/pages/wordlibrary.vue"],"version":"5.6.3"} \ No newline at end of file