158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
"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">
|
||
搜索结果:“{query}”
|
||
</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>
|
||
);
|
||
}
|