diff --git a/nginx.conf b/nginx.conf index 9ed92e8..bb06b14 100644 --- a/nginx.conf +++ b/nginx.conf @@ -25,8 +25,11 @@ server { } # API 反代 → Granian(ADRF 异步视图) - # 路由分散在 /api /user /bug /article /chat /learn /message /tool /history /media 等前缀下 - location ~ ^/(api|user|bug|article|chat|learn|message|tool|history|media|search|app|logs|s|air-quality|weather|currency|shorturl)/ { + # C-01:精确收窄到后端真实存在的 API 前缀。SPA 路由(/open-api*、/post/*、 + # /user-home*、/articles、/learn 列表页等)一律落到下面的 `location /` 回退, + # 不再被宽正则劫持。旧分享链接(/article/:id、/user/:id、/api-detail/* 等) + # 由前端 SPA 内 重定向承接,nginx 直接放行到 index.html 即可。 + location ~ ^/api/ { proxy_pass http://backend:8000; proxy_http_version 1.1; proxy_set_header Host $host; @@ -39,6 +42,165 @@ server { client_max_body_size 50M; } + # 后端 user.urls(/user//):SPA 已迁到 /user-home*,旧 /user/:id 分享 + # 链接由前端重定向承接;数字 id 形态放行到 SPA,其余代理到后端 + location ~ ^/user/\d+/?$ { + try_files $uri $uri/ /index.html; + } + location ~ ^/user/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Content-Type $http_content_type; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 bug.urls 仅 /bug/reports/*;SPA /bug 与 /bug-detail 放行 + location ~ ^/bug/reports/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 article.urls 仅 articles|my-articles|comments;SPA /post/*、/articles 放行 + location ~ ^/article/(articles|my-articles|comments)/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 learn.urls 子段;SPA /learn 列表页放行 + location ~ ^/learn/(courses|chapters|my-courses|my-progress|materials|favorites|cdn)/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 message.urls(/message/*);SPA /messages(多 s)天然不命中 + location ~ ^/message/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 chat.urls 子段;SPA /chat 本体放行 + location ~ ^/chat/(friend-requests|friends|users|conversations|messages|upload|favorite-stickers)/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 tool.urls(/tool/*);SPA /tool-detail 放行(不以 /tool/ 开头) + location ~ ^/tool/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 history.urls 仅 records/;SPA /history 列表页放行 + location ~ ^/history/records/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 后端 search 查询串形态(GET /search/?q=);裸 /search(页)与无参 /search/ 放行到 SPA。 + # location 按 URI 匹配、看不见 query string,故用 $args 分流。 + location = /search/ { + error_page 418 = @spa_fallback; + if ($args = "") { return 418; } + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + location ~ ^/search/(suggestions|hot-keywords)/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + + # 后端 app(changelog)/ logs / shorturl + location ~ ^/(app|logs/api|shorturl)/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_max_body_size 50M; + } + + # 短链跳转 /s//:仅短码形态代理,其余(/settings、/shorturl-detail)放行 + location ~ ^/s/[^/]+/?$ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + # Django admin / swagger / redoc / i18n / 测试页 location /admin/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 50M; } location /swagger { proxy_pass http://backend:8000; proxy_set_header Host $host; } @@ -66,8 +228,23 @@ server { add_header Cache-Control "no-cache, no-store, must-revalidate"; } + # C-05 SEO 预渲染优先:蜘蛛/首屏直接拿到含正文的静态 HTML, + # React hydrate 后照常接管交互(预渲染页内含 location.replace 跳转到 SPA 路由)。 + # 首页用 index.seo.html(不覆盖 SPA 外壳 index.html);工具页/聚合页为目录 index.html。 + location = / { + try_files /index.seo.html /index.html; + } + location ~ ^/(utility/[a-z-]+|qrcode-generator|color-picker|baidu-translate|utility|top)/?$ { + try_files $uri $uri/index.html $uri/ /index.html; + add_header Cache-Control "public, max-age=3600"; + } + # SPA 路由回退(放在最后,避免拦截上面的 API 请求) location / { try_files $uri $uri/ /index.html; } + + location @spa_fallback { + try_files $uri $uri/ /index.html; + } } diff --git a/scripts/check_routes.mjs b/scripts/check_routes.mjs new file mode 100644 index 0000000..201bda4 --- /dev/null +++ b/scripts/check_routes.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/* C-01 路由回归脚本:73 条 SPA 路由直访检查。 + * 检测三件套:① Django 404/JSON 劫持 ② #root 空(白屏)③ 正文长度。 + * 用法:node scripts/check_routes.mjs --base http://127.0.0.1:5173 + * 期望:PASS 73/73,hijacked=0,blank=0 + */ +import { readFileSync } from 'node:fs' + +const args = process.argv.slice(2) +const baseIdx = args.indexOf('--base') +const BASE = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1].replace(/\/$/, '') : 'http://127.0.0.1:5173' + +// 与 src/App.tsx 的 一一对应(不含 * 兜底) +const ROUTES = [ + '/', '/settings', '/history', '/favorites', + '/open-api-docs', '/open-api-directory', + '/profile', '/profile/email', '/profile/phone', '/profile/change-password', + '/use', '/document', '/open-api', '/utility', '/learn', '/articles', '/bug', + '/search', '/messages', '/chat', + '/open-api-detail/1', '/tool-detail', '/course-learn', '/post/1', + '/privacy', '/agreement', '/manual', '/system-message', + '/user-home/1', '/user-home', '/console', + '/article-manage', '/bug-detail', '/article-editor', '/learn-manage', '/learn-editor', + '/baidu-translate', + '/wallet/points', '/wallet/coins', '/wallet/coins/recharge', '/task-center', '/wallet/invite', + '/invite/abc123', '/forgot-password', '/register', + '/utility/json-formatter', '/utility/base64', '/utility/timestamp', '/utility/regex', + '/utility/code-runner', '/utility/text-diff', '/utility/encoding-converter', + '/utility/charset-converter', '/utility/color-converter', '/utility/code-formatter', + '/color-picker', '/qrcode-generator', '/utility/image-compressor', '/utility/calculator', + '/utility/date-calculator', '/utility/image-editor', '/changelog', + '/ip-location', '/currency-detail', + '/open-api-detail/currency', '/open-api-detail/baidu-translate', + '/open-api/air-quality-details', '/open-api/weather-details', + '/open-api/qrcode-generator', '/open-api/ip-location', '/open-api/password-generator', + '/shorturl-detail', + // 旧路径重定向(应返回 SPA,由前端 承接,不应被代理劫持) + '/api-docs', '/api-directory', '/api', '/api-detail/1', + '/api-detail/currency', '/api-detail/baidu-translate', + '/api/air-quality-details', '/api/weather-details', + '/api/qrcode-generator', '/api/ip-location', '/api/password-generator', + '/article/1', '/user', '/user/1', +] + +function isHijacked(status, text) { + if (status === 404 && /Django|Not Found|Page not found/i.test(text)) return true + const t = text.trim() + // 后端 JSON 特征:{"code":...} / {"detail":...} 且无前端根节点 + if (/^\s*\{[\s\S]*"(code|detail|message)"[\s\S]*\}\s*$/.test(t.slice(0, 2000)) && !text.includes('id="root"')) return true + return false +} + +// dev 模式下 index.html 的 #root 恒为空(客户端水合前), +// 因此"白屏"只能断言"是否返回了 SPA 外壳":含 id="root" 即算 SPA 到达。 +// 真正的渲染白屏由 Playwright 抽查覆盖(见 PROGRESS)。 +function isSpaShell(text) { + return text.includes('id="root"') +} + +let pass = 0, hijacked = 0, blank = 0 +const bad = [] +for (const r of ROUTES) { + const url = BASE + r + try { + const res = await fetch(url, { redirect: 'manual' }) + const text = await res.text() + const hj = isHijacked(res.status, text) + const bl = !hj && !isSpaShell(text) + if (hj) hijacked++ + if (bl) blank++ + if (!hj && !bl) { pass++; console.log(`ok ${r}`) } + else { bad.push(r); console.log(`${hj ? 'HIJACKED' : 'BLANK '} ${r} (status=${res.status} len=${text.length})`) } + } catch (e) { + bad.push(r) + console.log(`ERROR ${r} (${e.message})`) + } +} +console.log('---') +console.log(`ROUTES=${ROUTES.length} PASS=${pass} hijacked=${hijacked} blank=${blank}`) +if (bad.length) { console.log('BAD:'); bad.forEach((r) => console.log(' - ' + r)) } +process.exit(pass === ROUTES.length ? 0 : 1) diff --git a/src/App.tsx b/src/App.tsx index 22c5b2e..d343fda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import "./App.css"; -import { Route, Routes } from "react-router-dom"; +import { Navigate, Route, Routes, useParams } from "react-router-dom"; import { lazy, Suspense, useEffect } from "react"; import { Spin, App as AntdApp } from "antd"; @@ -41,6 +41,9 @@ const ApiDetail = lazy(() => import("@/pages/ApiDetail/ApiDetail")); const ApiDetailMobile = lazy(() => import("@/pages/ApiDetail/ApiDetailMobile")); const ToolDetail = lazy(() => import("@/pages/ToolDetail/ToolDetail")); const CourseLearn = lazy(() => import("@/pages/CourseLearn/CourseLearn")); +const CodeRunnerPage = lazy(() => import("@/pages/CodeRunnerPage/CodeRunnerPage")); +const ImageUpscaler = lazy(() => import("@/pages/ImageUpscaler/ImageUpscaler")); +const Top = lazy(() => import("@/pages/Top/Top")); const ArticleDetail = lazy(() => import("@/pages/ArticleDetail/ArticleDetail")); const Privacy = lazy(() => import("@/pages/Privacy/Privacy")); const UserAgreement = lazy(() => import("@/pages/UserAgreement/UserAgreement")); @@ -117,6 +120,20 @@ const PageLoading = () => ( ); +// C-01 旧路径重定向:保留已分享链接可用(带参透传) +function LegacyApiDetailRedirect() { + const { id } = useParams(); + return ; +} +function LegacyPostRedirect() { + const { id } = useParams(); + return ; +} +function LegacyUserHomeRedirect() { + const { id } = useParams(); + return ; +} + const App: React.FC = () => { const isMobile = useIsMobile(); const { message } = AntdApp.useApp(); @@ -152,15 +169,15 @@ const App: React.FC = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> } /> } /> } /> - : } /> + : } /> : } /> : } /> : } /> @@ -168,16 +185,16 @@ const App: React.FC = () => { } /> : } /> : } /> - : } /> + : } /> } /> } /> - } /> + } /> } /> : } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> @@ -197,6 +214,9 @@ const App: React.FC = () => { : } /> : } /> : } /> + } /> + } /> + } /> : } /> : } /> : } /> @@ -211,14 +231,29 @@ const App: React.FC = () => { : } /> } /> } /> - } /> - } /> - : } /> - : } /> - } /> - } /> - } /> + } /> + } /> + : } /> + : } /> + } /> + } /> + } /> : } /> + {/* C-01 旧路径重定向:已分享出去的链接不能全死 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/src/components/MobileBottomNav/MobileBottomNav.tsx b/src/components/MobileBottomNav/MobileBottomNav.tsx index ca2b886..b00fc16 100644 --- a/src/components/MobileBottomNav/MobileBottomNav.tsx +++ b/src/components/MobileBottomNav/MobileBottomNav.tsx @@ -18,7 +18,7 @@ const MobileBottomNav: React.FC = () => { const tabs = [ { key: "home", icon: , label: t("nav.home"), path: "/" }, - { key: "api", icon: , label: t("nav.tools"), path: "/api" }, + { key: "api", icon: , label: t("nav.tools"), path: "/open-api" }, { key: "utility", icon: , label: t("nav.utility"), path: "/utility" }, { key: "articles", icon: , label: t("nav.learn"), path: "/learn" }, { key: "profile", icon: , label: t("nav.bug"), path: "/bug" }, @@ -28,7 +28,7 @@ const MobileBottomNav: React.FC = () => { const path = location.pathname; if (path === "/") { setActiveTab("home"); - } else if (path.startsWith("/api")) { + } else if (path.startsWith("/open-api")) { setActiveTab("api"); } else if (path.startsWith("/utility")) { setActiveTab("utility"); diff --git a/src/components/MobileGuard/MobileGuard.tsx b/src/components/MobileGuard/MobileGuard.tsx index 2b3c0ca..c72ba07 100644 --- a/src/components/MobileGuard/MobileGuard.tsx +++ b/src/components/MobileGuard/MobileGuard.tsx @@ -10,7 +10,7 @@ import "./MobileGuard.css"; const MOBILE_ROUTES: Record = { "/": "/", - "/api": "/api", + "/open-api": "/open-api", "/utility": "/utility", "/learn": "/learn", "/articles": "/articles", diff --git a/src/components/MobileTopNav/MobileTopNav.tsx b/src/components/MobileTopNav/MobileTopNav.tsx index 5a949c5..e1c053c 100644 --- a/src/components/MobileTopNav/MobileTopNav.tsx +++ b/src/components/MobileTopNav/MobileTopNav.tsx @@ -100,7 +100,7 @@ const MobileTopNav: React.FC = () => { openLoginModal(); return; } - navigate("/user"); + navigate("/user-home"); }} > {isLogin ? ( diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index d47a2c6..3fe2111 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -91,7 +91,7 @@ const Navbar: React.FC = () => { const path = location.pathname; const routeMap: Record = { "/": "home", - "/api": "api", + "/open-api": "api", "/utility": "utility", "/learn": "learn", "/articles": "articles", @@ -101,13 +101,13 @@ const Navbar: React.FC = () => { }; if (routeMap[path]) { setCurrentMenuItem(routeMap[path]); - } else if (path.startsWith("/api/") || path.startsWith("/api-detail")) { + } else if (path.startsWith("/open-api/") || path.startsWith("/open-api-detail")) { setCurrentMenuItem("api"); } else if (path.startsWith("/utility/") || path.startsWith("/tool-detail")) { setCurrentMenuItem("utility"); } else if (path.startsWith("/learn/") || path.startsWith("/course-learn")) { setCurrentMenuItem("learn"); - } else if (path.startsWith("/articles/") || path.startsWith("/article/")) { + } else if (path.startsWith("/articles/") || path.startsWith("/post/")) { setCurrentMenuItem("articles"); } else if (path.startsWith("/bug") || path.startsWith("/bug-detail")) { setCurrentMenuItem("bug"); @@ -437,7 +437,7 @@ const Navbar: React.FC = () => { setCurrentMenuItem(e.key); } if (e.key === "home") navigation("/"); - if (e.key === "api") navigation("/api"); + if (e.key === "api") navigation("/open-api"); if (e.key === "utility") navigation("/utility"); if (e.key === "learn") navigation("/learn"); if (e.key === "articles") navigation("/articles"); @@ -525,7 +525,7 @@ const Navbar: React.FC = () => { dispatch(logout()); navigation("/"); } else if (info.key === "userhome") { - navigation("/user"); + navigation("/user-home"); } else if (info.key === "console-articles") { navigation("/article-manage"); } else if (info.key === "wallet-points") { @@ -552,12 +552,12 @@ const Navbar: React.FC = () => { size={40} className="navbar-avatar" icon={} - onClick={() => navigation("/user")} + onClick={() => navigation("/user-home")} style={{ cursor: "pointer" }} /> navigation("/user")} + onClick={() => navigation("/user-home")} style={{ cursor: "pointer" }} > {user?.username || t("nav.user")} diff --git a/src/pages/ApiDirectory/ApiDirectory.tsx b/src/pages/ApiDirectory/ApiDirectory.tsx index 8c19e01..f9181d0 100644 --- a/src/pages/ApiDirectory/ApiDirectory.tsx +++ b/src/pages/ApiDirectory/ApiDirectory.tsx @@ -193,26 +193,26 @@ const ApiDirectory: React.FC = () => { const getApiRoute = (item: ApiItem): string | null => { const pathMap: Record = { // 天气查询 - "/api/weather": "/api/weather-details", + "/api/weather": "/open-api/weather-details", // IP地址定位 - "/api/getIpData/": "/api/ip-location", + "/api/getIpData/": "/open-api/ip-location", // 空气质量 - "/api/air-quality/": "/api/air-quality-details", + "/api/air-quality/": "/open-api/air-quality-details", // 汇率相关 - "/api/currency/rates/": "/api-detail/currency", - "/api/currency/currencies/": "/api-detail/currency", - "/api/currency/convert/": "/api-detail/currency", + "/api/currency/rates/": "/open-api-detail/currency", + "/api/currency/currencies/": "/open-api-detail/currency", + "/api/currency/convert/": "/open-api-detail/currency", // 二维码生成 - "/api/qrcode-generator/": "/api/qrcode-generator", + "/api/qrcode-generator/": "/open-api/qrcode-generator", // 密码生成器 - "/api/password-generator/": "/api/password-generator", + "/api/password-generator/": "/open-api/password-generator", // 工具类 "/api/tool/text-diff": "/utility/text-diff", "/api/tool/image-compressor": "/utility/image-compressor", // 短链接 "/api/shorturl/shorten": "/shorturl-detail", // 百度翻译 - "/api/baiduFanyi": "/api-detail/baidu-translate", + "/api/baiduFanyi": "/open-api-detail/baidu-translate", }; return pathMap[item.url_path] || null; }; diff --git a/src/pages/ApiDirectory/ApiDirectoryDesktop.tsx b/src/pages/ApiDirectory/ApiDirectoryDesktop.tsx index 4e3c894..360b0a7 100644 --- a/src/pages/ApiDirectory/ApiDirectoryDesktop.tsx +++ b/src/pages/ApiDirectory/ApiDirectoryDesktop.tsx @@ -76,13 +76,13 @@ const ApiDirectoryDesktop: React.FC = () => { description: api.description, image: getApiImageUrl(api), category: api.category_name || 'API接口', - link: `/api-detail/${api.id}`, + link: `/open-api-detail/${api.id}`, }); } catch (err) { console.error('记录浏览历史失败:', err); } } - navigate(`/api-detail/${api.id}`); + navigate(`/open-api-detail/${api.id}`); }; const handleFavoriteToggle = async (apiId: number, e: React.MouseEvent) => { diff --git a/src/pages/ApiDirectory/ApiDirectoryMobile.tsx b/src/pages/ApiDirectory/ApiDirectoryMobile.tsx index a903a77..15aad9f 100644 --- a/src/pages/ApiDirectory/ApiDirectoryMobile.tsx +++ b/src/pages/ApiDirectory/ApiDirectoryMobile.tsx @@ -112,13 +112,13 @@ const ApiDirectoryMobile: React.FC = () => { description: api.description, image: getApiImageUrl(api), category: api.category_name || 'API接口', - link: `/api-detail/${api.id}`, + link: `/open-api-detail/${api.id}`, }); } catch (err) { console.error('记录浏览历史失败:', err); } } - navigate(`/api-detail/${api.id}`); + navigate(`/open-api-detail/${api.id}`); }; if (loading) { diff --git a/src/pages/ArticleDetail/ArticleDetail.tsx b/src/pages/ArticleDetail/ArticleDetail.tsx index e02e39f..3f2dc4b 100644 --- a/src/pages/ArticleDetail/ArticleDetail.tsx +++ b/src/pages/ArticleDetail/ArticleDetail.tsx @@ -78,7 +78,7 @@ const ArticleDetail: React.FC = () => { description: article.excerpt || "", image: article.image || "", category: article.category || "", - link: `/article/${id}`, + link: `/post/${id}`, } : null); useEffect(() => { @@ -333,12 +333,12 @@ const ArticleDetail: React.FC = () => { src={article.author_avatar} size={32} style={{ cursor: "pointer" }} - onClick={() => navigate(`/user/${article.author_user_id}`)} + onClick={() => navigate(`/user-home/${article.author_user_id}`)} /> navigate(`/user/${article.author_user_id}`)} + onClick={() => navigate(`/user-home/${article.author_user_id}`)} > {article.author_name} @@ -419,7 +419,7 @@ const ArticleDetail: React.FC = () => {
navigate(`/article/${related.id}`)} + onClick={() => navigate(`/post/${related.id}`)} >
{related.title} @@ -444,14 +444,14 @@ const ArticleDetail: React.FC = () => { src={article.author_avatar} size={64} style={{ cursor: "pointer" }} - onClick={() => navigate(`/user/${article.author_user_id}`)} + onClick={() => navigate(`/user-home/${article.author_user_id}`)} />
navigate(`/user/${article.author_user_id}`)} + onClick={() => navigate(`/user-home/${article.author_user_id}`)} > {article.author_name}
@@ -514,14 +514,14 @@ const ArticleDetail: React.FC = () => { src={comment.user_avatar} size={40} style={{ cursor: "pointer" }} - onClick={() => comment.user_id && navigate(`/user/${comment.user_id}`)} + onClick={() => comment.user_id && navigate(`/user-home/${comment.user_id}`)} />
comment.user_id && navigate(`/user/${comment.user_id}`)} + onClick={() => comment.user_id && navigate(`/user-home/${comment.user_id}`)} > {comment.user_name} @@ -554,14 +554,14 @@ const ArticleDetail: React.FC = () => { src={reply.user_avatar} size={32} style={{ cursor: "pointer" }} - onClick={() => reply.user_id && navigate(`/user/${reply.user_id}`)} + onClick={() => reply.user_id && navigate(`/user-home/${reply.user_id}`)} />
reply.user_id && navigate(`/user/${reply.user_id}`)} + onClick={() => reply.user_id && navigate(`/user-home/${reply.user_id}`)} > {reply.user_name} diff --git a/src/pages/ArticleManage/ArticleManage.tsx b/src/pages/ArticleManage/ArticleManage.tsx index 421f953..a7bd7e5 100644 --- a/src/pages/ArticleManage/ArticleManage.tsx +++ b/src/pages/ArticleManage/ArticleManage.tsx @@ -215,7 +215,7 @@ const ArticleManage: React.FC = () => { ], onClick: ({ key }: { key: string }) => { switch (key) { - case "view": navigate(`/article/${record.id}`); break; + case "view": navigate(`/post/${record.id}`); break; case "edit": navigate(`/article-editor?id=${record.id}`); break; case "copy": navigator.clipboard?.writeText(`${window.location.origin}/article/${record.id}`); @@ -239,7 +239,7 @@ const ArticleManage: React.FC = () => {
@@ -297,7 +297,7 @@ const ArticleManage: React.FC = () => {