fix(ui): fix auth routing, i18n init, and component lifecycle issues

- useAuth: replace watcher with explicit router.replace on login/logout
- useMyI18n: create single i18n instance at module level (avoid dupes)
- usePasskey: detect WebAuthn support synchronously (no onMounted)
- main.ts: import i18n from hook module instead of calling composable
- ab-add-rss: hoist useApi composables outside functions to avoid
  recreating them on each call
- calendar: prevent duplicate refreshes when already refreshing
- downloader: guard interval polling against stale activation state
- router: only mark setupChecked on successful status check
- log store: stop SSE log updates on logout
- i18n: add missing "edit" translation key (en + zh-CN)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Estrella Pan
2026-02-23 11:46:56 +01:00
parent 52580d08c8
commit e82e6ab128
12 changed files with 87 additions and 78 deletions

2
backend/uv.lock generated
View File

@@ -61,7 +61,7 @@ wheels = [
[[package]] [[package]]
name = "auto-bangumi" name = "auto-bangumi"
version = "3.2.3b4" version = "3.2.3b5"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },

View File

@@ -30,6 +30,61 @@ const loading = reactive({
subscribe: false, subscribe: false,
}); });
const { execute: addRssAggregate } = useApi(apiRSS.add, {
showMessage: true,
onBeforeExecute() {
loading.analyze = true;
},
onSuccess() {
show.value = false;
},
onFinally() {
loading.analyze = false;
},
});
const { execute: analyzeRss } = useApi(apiDownload.analysis, {
showMessage: true,
onBeforeExecute() {
loading.analyze = true;
},
onSuccess(res) {
rule.value = res;
step.value = 'confirm';
},
onFinally() {
loading.analyze = false;
},
});
const { execute: executeCollect } = useApi(apiDownload.collection, {
showMessage: true,
onBeforeExecute() {
loading.collect = true;
},
onSuccess() {
getAll();
show.value = false;
},
onFinally() {
loading.collect = false;
},
});
const { execute: executeSubscribe } = useApi(apiDownload.subscribe, {
showMessage: true,
onBeforeExecute() {
loading.subscribe = true;
},
onSuccess() {
getAll();
show.value = false;
},
onFinally() {
loading.subscribe = false;
},
});
// Computed // Computed
const posterSrc = computed(() => resolvePosterUrl(rule.value.poster_link)); const posterSrc = computed(() => resolvePosterUrl(rule.value.poster_link));
@@ -82,34 +137,9 @@ function addRss() {
} }
if (rss.value.aggregate) { if (rss.value.aggregate) {
// Aggregate mode: directly add RSS addRssAggregate(rss.value);
useApi(apiRSS.add, {
showMessage: true,
onBeforeExecute() {
loading.analyze = true;
},
onSuccess() {
show.value = false;
},
onFinally() {
loading.analyze = false;
},
}).execute(rss.value);
} else { } else {
// Single mode: analyze and show confirm analyzeRss(rss.value);
useApi(apiDownload.analysis, {
showMessage: true,
onBeforeExecute() {
loading.analyze = true;
},
onSuccess(res) {
rule.value = res;
step.value = 'confirm';
},
onFinally() {
loading.analyze = false;
},
}).execute(rss.value);
} }
} }
@@ -146,36 +176,12 @@ async function autoDetectOffset() {
function collect() { function collect() {
if (!rule.value) return; if (!rule.value) return;
useApi(apiDownload.collection, { executeCollect(rule.value);
showMessage: true,
onBeforeExecute() {
loading.collect = true;
},
onSuccess() {
getAll();
show.value = false;
},
onFinally() {
loading.collect = false;
},
}).execute(rule.value);
} }
function subscribe() { function subscribe() {
if (!rule.value) return; if (!rule.value) return;
useApi(apiDownload.subscribe, { executeSubscribe(rule.value, rss.value);
showMessage: true,
onBeforeExecute() {
loading.subscribe = true;
},
onSuccess() {
getAll();
show.value = false;
},
onFinally() {
loading.subscribe = false;
},
}).execute(rule.value, rss.value);
} }
</script> </script>

View File

@@ -9,14 +9,6 @@ export const useAuth = createSharedComposable(() => {
const isLoggedIn = useLocalStorage('isLoggedIn', false); const isLoggedIn = useLocalStorage('isLoggedIn', false);
watch(isLoggedIn, (v) => {
if (v) {
router.replace({ name: 'Index' });
} else {
router.replace({ name: 'Login' });
}
});
const user = reactive<User>({ const user = reactive<User>({
username: '', username: '',
password: '', password: '',
@@ -51,6 +43,7 @@ export const useAuth = createSharedComposable(() => {
isLoggedIn.value = true; isLoggedIn.value = true;
clearUser(); clearUser();
message.success(t('notify.login_success')); message.success(t('notify.login_success'));
router.replace({ name: 'Index' });
}) })
.catch((err: ApiError) => { .catch((err: ApiError) => {
if (err.status === 404) { if (err.status === 404) {
@@ -64,6 +57,7 @@ export const useAuth = createSharedComposable(() => {
onSuccess() { onSuccess() {
clearUser(); clearUser();
isLoggedIn.value = false; isLoggedIn.value = false;
router.replace({ name: 'Login' });
}, },
}); });

View File

@@ -16,18 +16,20 @@ function normalizeLocale(locale: string): Languages {
return 'en'; return 'en';
} }
export const i18n = createI18n({
legacy: false,
locale: normalizeLocale(navigator.language),
fallbackLocale: 'en',
messages,
});
export const useMyI18n = createSharedComposable(() => { export const useMyI18n = createSharedComposable(() => {
const lang = useLocalStorage<Languages>( const lang = useLocalStorage<Languages>(
'lang', 'lang',
normalizeLocale(navigator.language) normalizeLocale(navigator.language)
); );
const i18n = createI18n({ i18n.global.locale.value = lang.value as unknown as Languages;
legacy: false,
locale: lang.value,
fallbackLocale: 'en',
messages,
});
watch(lang, (val) => { watch(lang, (val) => {
i18n.global.locale.value = val as unknown as Languages; i18n.global.locale.value = val as unknown as Languages;

View File

@@ -15,12 +15,7 @@ export const usePasskey = createSharedComposable(() => {
// 状态 // 状态
const passkeys = ref<PasskeyItem[]>([]); const passkeys = ref<PasskeyItem[]>([]);
const loading = ref(false); const loading = ref(false);
const isSupported = ref(false); const isSupported = ref(isWebAuthnSupported());
// 检测浏览器支持
onMounted(() => {
isSupported.value = isWebAuthnSupported();
});
// 加载 Passkey 列表 // 加载 Passkey 列表
async function loadPasskeys() { async function loadPasskeys() {

View File

@@ -151,6 +151,7 @@
"delete": "Delete", "delete": "Delete",
"delete_hit": "Delete Local File?", "delete_hit": "Delete Local File?",
"disable": "Disable", "disable": "Disable",
"edit": "Edit",
"edit_rule": "Edit Rule", "edit_rule": "Edit Rule",
"enable": "Enable", "enable": "Enable",
"enable_hit": "Do you want to enable this rule?", "enable_hit": "Do you want to enable this rule?",

View File

@@ -151,6 +151,7 @@
"delete": "删除", "delete": "删除",
"delete_hit": "是否删除本地文件?", "delete_hit": "是否删除本地文件?",
"disable": "禁用", "disable": "禁用",
"edit": "编辑",
"edit_rule": "编辑规则", "edit_rule": "编辑规则",
"enable": "启用", "enable": "启用",
"enable_hit": "确定启用该规则?", "enable_hit": "确定启用该规则?",

View File

@@ -1,13 +1,13 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { router } from './router'; import { router } from './router';
import { i18n } from './hooks/useMyI18n';
import App from './App.vue'; import App from './App.vue';
import '@unocss/reset/tailwind-compat.css'; import '@unocss/reset/tailwind-compat.css';
import 'virtual:uno.css'; import 'virtual:uno.css';
const pinia = createPinia(); const pinia = createPinia();
const { i18n } = useMyI18n();
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);

View File

@@ -25,6 +25,7 @@ async function refreshCalendar() {
} }
onActivated(() => { onActivated(() => {
if (refreshing.value) return;
refreshCalendar(); refreshCalendar();
}); });

View File

@@ -24,17 +24,20 @@ const isNull = computed(() => {
return config.value.downloader.host === ''; return config.value.downloader.host === '';
}); });
const isActive = ref(false);
const { pause, resume } = useIntervalFn(getAll, 5000, { immediate: false }); const { pause, resume } = useIntervalFn(getAll, 5000, { immediate: false });
onActivated(async () => { onActivated(async () => {
isActive.value = true;
await getConfig(); await getConfig();
if (!isNull.value) { if (isActive.value && !isNull.value) {
getAll(); getAll();
resume(); resume();
} }
}); });
onDeactivated(() => { onDeactivated(() => {
isActive.value = false;
pause(); pause();
clearSelection(); clearSelection();
}); });

View File

@@ -16,10 +16,10 @@ router.beforeEach(async (to) => {
try { try {
const status = await apiSetup.getStatus(); const status = await apiSetup.getStatus();
needSetup = status.need_setup; needSetup = status.need_setup;
setupChecked = true;
} catch { } catch {
// If check fails, proceed normally // If check fails, retry on next navigation
} }
setupChecked = true;
} }
// Redirect to setup if needed // Redirect to setup if needed

View File

@@ -27,6 +27,12 @@ export const useLogStore = defineStore('log', () => {
immediateCallback: true, immediateCallback: true,
}); });
watch(isLoggedIn, (loggedIn) => {
if (!loggedIn) {
offUpdate();
}
});
const { copy: clipboardCopy, isSupported: clipboardSupported } = useClipboard({ const { copy: clipboardCopy, isSupported: clipboardSupported } = useClipboard({
legacy: true, legacy: true,
}); });