From 65a8e0b127cc07736d25ec9f1f88f5e7421ef8f9 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Fri, 17 Apr 2026 15:04:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E4=BC=97=E7=AB=AF=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E7=99=BB=E5=BD=95=E6=97=B6=E8=AE=B0=E5=BD=95=20redire?= =?UTF-8?q?ct=EF=BC=8C=E7=99=BB=E5=BD=95=E5=90=8E=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=9B=9E=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- lesingle-creation-frontend/src/api/public.ts | 29 +++++++++++++++--- .../src/views/public/Login.vue | 30 +++++++++++++++++-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/lesingle-creation-frontend/src/api/public.ts b/lesingle-creation-frontend/src/api/public.ts index 67a3ff4..c8b0c6b 100644 --- a/lesingle-creation-frontend/src/api/public.ts +++ b/lesingle-creation-frontend/src/api/public.ts @@ -1,5 +1,26 @@ import axios from "axios"; +/** 公众端登录页路径(与路由一致) */ +const PUBLIC_LOGIN_PATH = "/p/login"; + +/** + * 跳转到公众端登录页,并携带「登录后回到当前页」的 redirect(pathname + search + hash) + */ +function redirectToPublicLogin(): void { + const path = window.location.pathname + window.location.search + window.location.hash; + // 已在登录页:不再叠加 redirect,避免循环 + if ( + path === PUBLIC_LOGIN_PATH || + path.startsWith(PUBLIC_LOGIN_PATH + "?") || + path.startsWith(PUBLIC_LOGIN_PATH + "#") + ) { + window.location.href = PUBLIC_LOGIN_PATH; + return; + } + const redirect = encodeURIComponent(path); + window.location.href = `${PUBLIC_LOGIN_PATH}?redirect=${redirect}`; +} + // 公众端专用 axios 实例 const publicApi = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || "/api", @@ -14,9 +35,9 @@ publicApi.interceptors.request.use((config) => { if (isTokenExpired(token)) { localStorage.removeItem("public_token"); localStorage.removeItem("public_user"); - // 如果在公众端页面,跳转到登录页 + // 如果在公众端页面,跳转到登录页(记录当前路径) if (window.location.pathname.startsWith("/p/")) { - window.location.href = "/p/login"; + redirectToPublicLogin(); } return config; } @@ -70,9 +91,9 @@ publicApi.interceptors.response.use( if (error.response?.status === 401) { localStorage.removeItem("public_token"); localStorage.removeItem("public_user"); - // 如果在公众端页面,跳转到公众端登录 + // 如果在公众端页面,跳转到公众端登录(记录当前路径) if (window.location.pathname.startsWith("/p/")) { - window.location.href = "/p/login"; + redirectToPublicLogin(); } } return Promise.reject(error); diff --git a/lesingle-creation-frontend/src/views/public/Login.vue b/lesingle-creation-frontend/src/views/public/Login.vue index b47eb15..da2d197 100644 --- a/lesingle-creation-frontend/src/views/public/Login.vue +++ b/lesingle-creation-frontend/src/views/public/Login.vue @@ -86,6 +86,30 @@ import type { Rule } from "ant-design-vue/es/form" const router = useRouter() const route = useRoute() + +const DEFAULT_AFTER_LOGIN = "/p/activities" + +/** 解析登录成功后的跳转地址:仅允许站内 `/p/` 路径,防止开放重定向 */ +function resolveRedirectAfterLogin(raw: unknown): string { + if (typeof raw !== "string" || !raw.trim()) return DEFAULT_AFTER_LOGIN + try { + const decoded = decodeURIComponent(raw.trim()) + if (!decoded.startsWith("/p/")) return DEFAULT_AFTER_LOGIN + if (decoded.startsWith("//") || /:\/\/|^\s*javascript:/i.test(decoded)) { + return DEFAULT_AFTER_LOGIN + } + // 避免跳回登录页自身 + if ( + decoded.split("?")[0] === "/p/login" || + decoded.split("?")[0].startsWith("/p/login/") + ) { + return DEFAULT_AFTER_LOGIN + } + return decoded + } catch { + return DEFAULT_AFTER_LOGIN + } +} const loading = ref(false) const isRegister = ref(false) const loginMethod = ref<'password' | 'sms'>('password') @@ -294,9 +318,9 @@ const handleSubmit = async () => { return } - // 跳转 - const redirect = (route.query.redirect as string) || "/p/activities" - router.push(redirect) + // 跳转:优先 query.redirect(与 public API 拦截器记录一致) + const target = resolveRedirectAfterLogin(route.query.redirect) + router.push(target) } catch (error: any) { const msg = error?.response?.data?.message ||