feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库
This commit is contained in:
+104
-26
@@ -2,11 +2,14 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FavoriteButton } from "@/components/common/FavoriteButton";
|
||||
import { HeroBanner } from "@/components/common/HeroBanner";
|
||||
import StatsSummary from "@/components/common/StatsSummary";
|
||||
import { PRICING_LABEL, PRICING_COLOR } from "@/lib/constants";
|
||||
import Image from "next/image";
|
||||
|
||||
interface Tool {
|
||||
id: number;
|
||||
@@ -19,6 +22,18 @@ interface Tool {
|
||||
viewCount: number;
|
||||
}
|
||||
|
||||
interface TrendingTool {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
logoUrl: string | null;
|
||||
websiteUrl: string;
|
||||
pricingModel: string;
|
||||
viewCount: number;
|
||||
category: { name: string; slug: string } | null;
|
||||
}
|
||||
|
||||
interface FlatCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -41,6 +56,7 @@ interface HomePageClientProps {
|
||||
parentCategories: ParentCategory[];
|
||||
totalTools: number;
|
||||
totalSkills: number;
|
||||
trendingTools: TrendingTool[];
|
||||
}
|
||||
|
||||
export default function HomePageClient({
|
||||
@@ -48,6 +64,7 @@ export default function HomePageClient({
|
||||
parentCategories,
|
||||
totalTools,
|
||||
totalSkills,
|
||||
trendingTools,
|
||||
}: HomePageClientProps) {
|
||||
const router = useRouter();
|
||||
const [activeSection, setActiveSection] = useState<string>("");
|
||||
@@ -240,6 +257,61 @@ export default function HomePageClient({
|
||||
subSlogan="发现、探索、使用 — 让人工智能触手可及"
|
||||
/>
|
||||
|
||||
<StatsSummary />
|
||||
|
||||
{trendingTools.length > 0 && (
|
||||
<section className="mb-10">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-amber-50 to-orange-50 border border-amber-200/60 px-3 py-1">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-amber-500">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="text-sm font-semibold text-amber-700">近期热门</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">最近加入 · 最受关注</span>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{trendingTools.map((tool) => (
|
||||
<Link
|
||||
key={tool.id}
|
||||
href={`/tools/${tool.slug}`}
|
||||
className="group relative flex h-full min-h-[150px] flex-col overflow-hidden rounded-xl border border-border/60 bg-white p-4 transition-all duration-300 hover:border-amber-200 hover:shadow-lg hover:shadow-amber-500/5 hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-bl from-amber-100/40 to-transparent rounded-bl-2xl" />
|
||||
<div className="relative flex flex-1 flex-col">
|
||||
<div className="flex items-center gap-2.5 mb-2.5">
|
||||
{tool.logoUrl && (
|
||||
<Image src={tool.logoUrl} referrerPolicy="no-referrer" alt="" width={32} height={32} className="h-8 w-8 rounded-lg border border-border/40 object-contain p-0.5 bg-white" onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
const fallback = target.nextElementSibling as HTMLElement | null;
|
||||
if (fallback) fallback.classList.remove("hidden");
|
||||
}} />
|
||||
)}
|
||||
<div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted/40 text-[10px] font-bold text-muted-foreground ${tool.logoUrl ? "hidden" : ""}`}>
|
||||
{tool.name.slice(0, 2)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold truncate group-hover:text-primary transition-colors">{tool.name}</h3>
|
||||
{tool.category && (
|
||||
<span className="text-[10px] text-muted-foreground/60">{tool.category.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex-1 text-xs text-muted-foreground/70 line-clamp-2 leading-relaxed">{tool.description}</p>
|
||||
<div className="mt-2.5 flex items-center gap-2">
|
||||
<span className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium border ${PRICING_COLOR[tool.pricingModel] || "border-slate-200 bg-slate-50 text-slate-600"}`}>
|
||||
{PRICING_LABEL[tool.pricingModel] || tool.pricingModel}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground/50">{tool.viewCount} 次浏览</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{groupedCategories.map(({ parent, ownCat, children }) => {
|
||||
const allCatsInGroup = [ownCat, ...children].filter(Boolean) as FlatCategory[];
|
||||
if (allCatsInGroup.length === 0) return null;
|
||||
@@ -307,26 +379,36 @@ export default function HomePageClient({
|
||||
{visibleTools.length > 0 ? (
|
||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{visibleTools.map((tool) => (
|
||||
<Card
|
||||
<Link
|
||||
key={tool.id}
|
||||
className="group relative cursor-pointer border-border/50 bg-white transition-all duration-300 hover:border-primary/30 hover:shadow-[0_8px_24px_rgba(0,0,0,0.08),0_2px_8px_rgba(0,0,0,0.04)] hover:-translate-y-0.5 shadow-[0_1px_3px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.03)]"
|
||||
onClick={() => router.push(`/tools/${tool.slug}`)}
|
||||
href={`/tools/${tool.slug}`}
|
||||
className="group relative flex h-full min-h-[120px] flex-col"
|
||||
>
|
||||
<div className="absolute top-1.5 right-1.5 z-10" onClick={(e) => e.stopPropagation()}>
|
||||
<Card className="flex h-full flex-col border-border/50 bg-white transition-all duration-300 hover:border-primary/30 hover:shadow-[0_8px_24px_rgba(0,0,0,0.08),0_2px_8px_rgba(0,0,0,0.04)] hover:-translate-y-0.5 shadow-[0_1px_3px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.03)]">
|
||||
<div className="absolute top-1.5 right-1.5 z-10" role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); } }} onClick={(e) => e.stopPropagation()}>
|
||||
<FavoriteButton type="tool" id={tool.id} initialFavorited={false} />
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="flex flex-1 flex-col p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
{tool.logoUrl ? (
|
||||
<img
|
||||
src={tool.logoUrl}
|
||||
referrerPolicy="no-referrer"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg border border-border/40 bg-white object-contain p-0.5 transition-all duration-300 group-hover:scale-105 group-hover:border-primary/30 group-hover:shadow-sm"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); }}
|
||||
/>
|
||||
<div className="relative h-10 w-10 shrink-0">
|
||||
<Image
|
||||
src={tool.logoUrl}
|
||||
alt={tool.name}
|
||||
fill
|
||||
sizes="40px"
|
||||
className="rounded-lg border border-border/40 bg-white object-contain p-0.5 transition-all duration-300 group-hover:scale-105 group-hover:border-primary/30 group-hover:shadow-sm"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallback = parent.nextElementSibling as HTMLElement;
|
||||
if (fallback) fallback.classList.remove('hidden');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-lg border border-border/40 bg-gradient-to-br from-primary/10 to-primary/5 text-xs font-semibold text-primary transition-all duration-300 group-hover:scale-105 ${tool.logoUrl ? 'hidden' : ''}`}>
|
||||
{tool.name.charAt(0)}
|
||||
@@ -342,11 +424,12 @@ export default function HomePageClient({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2.5 line-clamp-2 text-xs leading-relaxed text-muted-foreground/80">
|
||||
<p className="mt-2.5 flex-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground/80">
|
||||
{tool.description || "暂无简介"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -357,19 +440,14 @@ export default function HomePageClient({
|
||||
|
||||
{hasMore && (
|
||||
<div className="mt-3 text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
const cat = activeTab === "all"
|
||||
? parent.slug
|
||||
: children.find((c) => c.slug === activeTab)?.slug || parent.slug;
|
||||
router.push(`/categories/${cat}`);
|
||||
}}
|
||||
<Link
|
||||
href={`/categories/${activeTab === "all"
|
||||
? parent.slug
|
||||
: children.find((c) => c.slug === activeTab)?.slug || parent.slug}`}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
查看全部 {displayTools.length} 个 →
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user