feat: 交互体验专项优化(7 项)

- 登录回跳贯通:邮箱登录成功后按 ?redirect= 回到原页面(投票/收藏/评论的登录拦截闭环)
- 收藏按钮的登录跳转补上 redirect 参数
- 营销路由组 loading.tsx 骨架屏(客户端导航即时反馈)
- 投票按钮/收藏爱心按下缩放微交互
- 移动端抽屉滑入动画(keyframes + animate-drawer-in)
- 工具列表空状态加'清除搜索与筛选'按钮(en/zh)
- 品牌 404 页(图标 + Back to home / Browse tools 双入口)
This commit is contained in:
AI Directory Platform
2026-09-08 20:17:11 +00:00
parent ed64eb9e76
commit 3a2adbec76
12 changed files with 89 additions and 15 deletions
+2 -1
View File
@@ -645,7 +645,8 @@
"sort": {
"latest": "Latest",
"trending": "Trending"
}
},
"clearFilters": "Clear search & filters"
},
"seo": {
"home": {
+2 -1
View File
@@ -653,7 +653,8 @@
"sort": {
"latest": "最新",
"trending": "最热"
}
},
"clearFilters": "清除搜索与筛选"
},
"seo": {
"home": {
+1
View File
@@ -314,6 +314,7 @@ function collectRoutes() {
entry.name === "page.tsx" ||
entry.name === "route.ts" ||
entry.name === "layout.tsx" ||
entry.name === "loading.tsx" ||
entry.name === "not-found.tsx" ||
entry.name === "manifest.ts" ||
entry.name === "sitemap.ts"
@@ -44,7 +44,12 @@ export function LoginForm() {
return;
}
router.push(`/${locale}/`);
// 登录成功后回跳原页面(投票/收藏/评论的登录拦截会带 ?redirect= 参数)
const redirect =
typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("redirect")
: null;
router.push(redirect || `/${locale}/`);
} catch (err) {
setError(t('errors.loginFailed'));
} finally {
@@ -53,7 +53,9 @@ export function FavoritesProvider({ children }: { children: React.ReactNode }) {
(toolId: string) => {
if (!isAuthenticated) {
toast(t("loginRequired"));
router.push(`/${locale}/login`);
router.push(
`/${locale}/login?redirect=${encodeURIComponent(window.location.pathname)}`
);
return;
}
@@ -161,7 +163,7 @@ export function FavoriteButton({
ctx.toggle(toolId);
}}
className={cn(
"rounded-full p-2 transition-colors",
"rounded-full p-2 transition-all active:scale-125",
favorited
? "text-red-500 hover:text-red-600"
: "text-muted-foreground/50 hover:text-red-500"
@@ -49,6 +49,7 @@ interface ToolsTranslations {
pricingPaid: string;
sortLatest: string;
sortTrending: string;
clearFilters: string;
}
interface UseCaseOption {
@@ -351,6 +352,12 @@ export function ToolsList({
<p className="text-muted-foreground">
{translations.noTools}
</p>
<button
onClick={() => router.push(`/${locale}/tools`)}
className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
>
{translations.clearFilters}
</button>
</div>
)}
@@ -186,7 +186,7 @@ export function VoteButton({
aria-pressed={voted}
aria-label={voted ? t("unvote") : t("vote")}
className={cn(
"flex h-fit min-w-[4.5rem] flex-col items-center gap-0.5 rounded-xl border px-3 py-2.5 transition-colors",
"flex h-fit min-w-[4.5rem] flex-col items-center gap-0.5 rounded-xl border px-3 py-2.5 transition-all active:scale-95",
voted
? "border-primary/60 bg-primary/10 text-primary"
: "border-border text-foreground hover:border-primary/40 hover:bg-primary/5"
@@ -205,7 +205,7 @@ export function VoteButton({
onClick={() => ctx.toggle(toolId)}
aria-pressed={voted}
className={cn(
"flex w-full items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium transition-colors",
"flex w-full items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium transition-all active:scale-[0.98]",
voted
? "border-primary/60 bg-primary/10 text-primary"
: "border-border text-foreground hover:bg-hover"
+1 -1
View File
@@ -127,7 +127,7 @@ export function ThemeHeader({
{/* Mobile 抽屉 */}
{open && (
<nav className="border-t border-border bg-background px-4 py-3 lg:hidden" aria-label="Mobile">
<nav className="animate-drawer-in border-t border-border bg-background px-4 py-3 lg:hidden" aria-label="Mobile">
{navItems.map((item) => (
<LocaleLink
key={item.href}
@@ -0,0 +1,21 @@
/**
* # 组件 MarketingLoading
* 说明:营销路由组的加载骨架屏 —— 客户端导航时即时反馈,替代"冻结"的旧页面。
*/
export default function Loading() {
return (
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6">
<div className="mx-auto mb-4 h-8 w-64 animate-pulse rounded-lg bg-secondary" />
<div className="mx-auto mb-10 h-4 w-96 max-w-full animate-pulse rounded bg-secondary" />
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-28 animate-pulse rounded-xl border border-border bg-secondary/50"
style={{ animationDelay: `${i * 90}ms` }}
/>
))}
</div>
</div>
);
}
@@ -92,6 +92,7 @@ export default async function ToolsPage({ searchParams }: ToolsPageProps) {
pricingPaid: t("pricing.paid"),
sortLatest: t("sort.latest"),
sortTrending: t("sort.trending"),
clearFilters: t("clearFilters"),
};
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://toolso.ai';
+27 -7
View File
@@ -1,16 +1,36 @@
import Link from 'next/link';
import { defaultLocale } from '@/i18n.config';
/**
* # 组件 NotFound
* 说明:品牌化 404 页(图标 + 双入口:首页 / 工具目录)。
*/
export default function NotFound() {
return (
<html>
<body>
<div style={{ textAlign: 'center', padding: '100px' }}>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for doesn&apos;t exist.</p>
<Link href={`/${defaultLocale}`}>
Go to Homepage
</Link>
<body style={{ margin: 0, fontFamily: 'system-ui, sans-serif', background: '#fafafa', color: '#0a0a0a' }}>
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
<div style={{ textAlign: 'center', maxWidth: '420px' }}>
<div style={{ width: 64, height: 64, margin: '0 auto 20px', borderRadius: 16, background: '#0a0a0a', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
<path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
</div>
<p style={{ fontSize: 13, letterSpacing: '0.2em', color: '#737373', margin: '0 0 8px' }}>404</p>
<h1 style={{ fontSize: 24, margin: '0 0 8px' }}>Page not found</h1>
<p style={{ fontSize: 14, color: '#525252', lineHeight: 1.6, margin: '0 0 24px' }}>
The page you are looking for doesn&apos;t exist or may have moved.
</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
<Link href={`/${defaultLocale}`} style={{ display: 'inline-block', padding: '10px 20px', borderRadius: 8, background: '#0a0a0a', color: '#fff', textDecoration: 'none', fontSize: 14 }}>
Back to home
</Link>
<Link href={`/${defaultLocale}/tools`} style={{ display: 'inline-block', padding: '10px 20px', borderRadius: 8, border: '1px solid #e5e5e5', color: '#0a0a0a', textDecoration: 'none', fontSize: 14 }}>
Browse tools
</Link>
</div>
</div>
</div>
</body>
</html>
+15
View File
@@ -104,3 +104,18 @@ html {
text-wrap: balance;
}
}
/* 移动端抽屉滑入动画(交互反馈) */
@keyframes bu-drawer-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-drawer-in {
animation: bu-drawer-in 0.18s ease-out;
}