Files
GovAI/apps/web/src/app/(portal)/store/page.tsx
T
2026-06-15 23:48:37 +08:00

158 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useQuery } from "@tanstack/react-query";
import api from "@/lib/api";
import type { App, Category } from "@/lib/types";
import { useAuthStore } from "@/stores/auth";
import { AppCard } from "@/components/app-card/app-card";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Sparkles, LayoutGrid } from "lucide-react";
import type { LucideIcon } from "lucide-react";
function SectionHeader({
title,
icon: Icon,
href,
}: {
title: string;
icon?: LucideIcon;
href?: string;
}) {
return (
<div className="flex items-center justify-between mb-5">
<h2 className="text-lg font-bold flex items-center gap-2">
<span className="inline-block w-1 h-5 bg-blue-800 rounded-full mr-1" />
{Icon && <Icon className="h-5 w-5 text-blue-700" />}
{title}
</h2>
{href && (
<Link
href={href}
className="text-sm text-blue-700 hover:text-blue-900 font-medium transition-colors"
>
</Link>
)}
</div>
);
}
function AppGridSkeleton({ count = 4 }: { count?: number }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: count }).map((_, i) => (
<Skeleton key={i} className="h-36 rounded-lg" />
))}
</div>
);
}
export default function StorePage() {
const searchParams = useSearchParams();
const query = searchParams.get("q") || "";
const { user } = useAuthStore();
const orgId = user?.org_id || "";
const orgParam = orgId ? `org_id=${orgId}` : "";
const { data: categories } = useQuery({
queryKey: ["categories", orgId],
queryFn: () => api.get<Category[]>(`/api/v1/store/categories?${orgParam}`),
});
const { data: featured, isLoading: featuredLoading } = useQuery({
queryKey: ["featured", orgId],
queryFn: () => api.get<App[]>(`/api/v1/store/featured?${orgParam}`),
enabled: !query,
});
const { data: topApps, isLoading: topLoading } = useQuery({
queryKey: ["topApps", orgId],
queryFn: () => api.get<App[]>(`/api/v1/store/rankings?${orgParam}`),
enabled: !query,
});
const { data: searchResults, isLoading: searchLoading } = useQuery({
queryKey: ["search", query, orgId],
queryFn: () => api.get<{ items: App[] }>(`/api/v1/store/apps?q=${encodeURIComponent(query)}&${orgParam}`),
enabled: !!query,
});
if (query) {
return (
<div className="mx-auto w-full max-w-7xl px-3 md:px-6 lg:px-8 py-4 md:py-6">
<h1 className="text-xl font-bold mb-4">
&ldquo;{query}&rdquo;
</h1>
{searchLoading ? (
<AppGridSkeleton count={8} />
) : searchResults?.items?.length ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{searchResults.items.map((app) => (
<AppCard key={app.id} app={app} />
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
</div>
)}
</div>
);
}
return (
<div className="mx-auto w-full max-w-7xl px-3 md:px-6 lg:px-8 py-4 md:py-8 space-y-6 md:space-y-10">
{/* Featured */}
<section>
<SectionHeader title="推荐应用" icon={Sparkles} />
{featuredLoading ? (
<AppGridSkeleton />
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{featured?.map((app) => (
<AppCard key={app.id} app={app} />
))}
</div>
)}
</section>
{/* Categories */}
<section>
<SectionHeader title="应用分类" icon={LayoutGrid} />
<div className="flex flex-wrap gap-2">
<Link href="/store">
<Badge variant="default" className="cursor-pointer"></Badge>
</Link>
{categories?.map((cat) => (
<Link key={cat.id} href={`/store/category/${cat.slug}`}>
<Badge variant="secondary" className="cursor-pointer hover:bg-secondary/80">
{cat.name}
{cat.app_count != null && cat.app_count > 0 && (
<span className="ml-1 text-muted-foreground">{cat.app_count}</span>
)}
</Badge>
</Link>
))}
</div>
</section>
{/* All Apps */}
<section>
<SectionHeader title="全部应用" icon={LayoutGrid} />
{topLoading ? (
<AppGridSkeleton />
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{topApps?.map((app) => (
<AppCard key={app.id} app={app} />
))}
</div>
)}
</section>
</div>
);
}