diff --git a/conf/emotion-museum.conf b/conf/emotion-museum.conf index 2430b38..a232d84 100644 --- a/conf/emotion-museum.conf +++ b/conf/emotion-museum.conf @@ -5,6 +5,11 @@ server { listen 80; server_name 101.200.208.45; + # 根路径不提供站点,避免跳转或兜底到其他 server + location = / { + return 404; + } + # 前端应用路径 location /emotion-museum/ { alias /data/www/emotion-museum/; @@ -95,7 +100,9 @@ server { # 处理不带末尾斜杠的 /course-of-life 请求 location = /course-of-life { - rewrite ^(.*)$ $1/ permanent; + # 不进行 301/302 外部跳转:内部改写到 /course-of-life/ 交给下方 SPA location 处理 + # 这样 URL 仍是 /course-of-life,但返回内容与 /course-of-life/ 完全一致(且不会触发“下载”) + rewrite ^ /course-of-life/ last; } # 后端 API 代理 diff --git a/life-script/src/App.jsx b/life-script/src/App.jsx index 51cdf30..9045eff 100644 --- a/life-script/src/App.jsx +++ b/life-script/src/App.jsx @@ -17,19 +17,20 @@ import useStore from './store/useStore'; const ProtectedRoute = ({ children, requireAuth = false, requireOnboarding = false }) => { const { isLoggedIn, registrationData } = useStore(); const navigate = useNavigate(); + const hasToken = !!localStorage.getItem('access_token'); // 检查是否完成入站流程(有昵称和未来愿景即视为已完成) const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision); useEffect(() => { - if (requireAuth && !isLoggedIn) { + if (requireAuth && (!isLoggedIn || !hasToken)) { navigate('/', { replace: true }); } else if (requireOnboarding && !hasCompletedOnboarding) { navigate('/onboarding', { replace: true }); } - }, [isLoggedIn, hasCompletedOnboarding, requireAuth, requireOnboarding, navigate]); + }, [isLoggedIn, hasCompletedOnboarding, requireAuth, requireOnboarding, hasToken, navigate]); - if (requireAuth && !isLoggedIn) { + if (requireAuth && (!isLoggedIn || !hasToken)) { return ; } @@ -66,6 +67,7 @@ const PageTransition = ({ children }) => { const AnimatedRoutes = () => { const location = useLocation(); const { isLoggedIn, registrationData } = useStore(); + const hasToken = !!localStorage.getItem('access_token'); // 检查是否完成入站流程(有昵称和未来愿景即视为已完成) const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision); @@ -77,7 +79,7 @@ const AnimatedRoutes = () => { ) : ( @@ -130,8 +132,8 @@ const AnimatedRoutes = () => { * App 主组件 */ function App() { - // 生产环境使用 /course-of-life 作为基础路径 - const basename = import.meta.env.PROD ? '/course-of-life' : ''; + // 使用 Vite 的 BASE_URL 并移除末尾斜杠,确保 BrowserRouter basename 兼容 + const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, ''); return ( diff --git a/life-script/src/services/api.js b/life-script/src/services/api.js index f4b95e0..4c88f8a 100644 --- a/life-script/src/services/api.js +++ b/life-script/src/services/api.js @@ -60,9 +60,13 @@ api.interceptors.response.use( localStorage.removeItem('refresh_token'); localStorage.removeItem('life_trajectory_v3'); // 清除 Zustand 持久化状态 - // 避免重复跳转导致的无限循环 - if (window.location.pathname !== '/') { - window.location.href = '/'; + // 避免重复跳转导致的无限循环;使用 Vite 的 BASE_URL 确保在子路径下跳转 + const baseUrl = (import.meta.env.BASE_URL || '/'); + const normalize = (p) => (p || '/').replace(/\/+$/, '') || '/'; + const at = normalize(window.location.pathname); + const target = normalize(baseUrl); + if (at !== target) { + window.location.href = baseUrl; // 仅当不在基路径时跳转 } return Promise.reject(new Error('未授权访问,请重新登录')); }