Files
ai_web/frontend/src/features/bounties/pages/Bounties.tsx
2026-01-28 16:00:56 +08:00

157 lines
5.7 KiB
TypeScript

import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Navbar } from "@/components/Navbar";
import { BOUNTY_STATUS_MAP } from "@/const";
import { useBounties, useCreateBounty } from "@/hooks/useApi";
import { useDebounce } from "@/hooks/useDebounce";
import { useState, useMemo } from "react";
import { toast } from "sonner";
import { getErrorCopy } from "@/lib/i18n/errorMessages";
import { Trophy, Sparkles, Plus, Loader2 } from "lucide-react";
import BountiesHeader from "@/features/bounties/components/BountiesHeader";
import BountiesGrid from "@/features/bounties/components/BountiesGrid";
const statusMap: Record<string, { label: string; class: string }> = {
open: { label: "开放中", class: "badge-open" },
in_progress: { label: "进行中", class: "badge-in-progress" },
completed: { label: "已完成", class: "badge-completed" },
cancelled: { label: "已取消", class: "badge-cancelled" },
disputed: { label: "争议中", class: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" },
};
export default function Bounties() {
const { user, isAuthenticated } = useAuth();
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [newBounty, setNewBounty] = useState({
title: "",
description: "",
reward: "",
deadline: "",
});
const { data: bountiesData, isLoading, refetch } = useBounties({
status: statusFilter === "all" ? undefined : statusFilter
});
const debouncedSearchQuery = useDebounce(searchQuery, 300);
const bounties = bountiesData?.items || [];
const createBountyMutation = useCreateBounty();
const filteredBounties = useMemo(() => {
if (!bounties) return [];
const query = debouncedSearchQuery.trim().toLowerCase();
if (!query) return bounties;
return bounties.filter((b) =>
b.title.toLowerCase().includes(query) ||
b.description.toLowerCase().includes(query)
);
}, [bounties, debouncedSearchQuery]);
const handleCreateBounty = () => {
if (!newBounty.title.trim()) {
toast.error("请输入悬赏标题");
return;
}
if (!newBounty.description.trim()) {
toast.error("请输入悬赏描述");
return;
}
const rewardValue = Number(newBounty.reward);
if (!newBounty.reward || !Number.isFinite(rewardValue) || rewardValue <= 0) {
toast.error("请输入有效的赏金金额");
return;
}
createBountyMutation.mutate({
title: newBounty.title,
description: newBounty.description,
reward: rewardValue.toFixed(2),
deadline: newBounty.deadline || undefined,
}, {
onSuccess: () => {
toast.success("悬赏发布成功!");
setIsCreateOpen(false);
setNewBounty({ title: "", description: "", reward: "", deadline: "" });
refetch();
},
onError: (error: unknown) => {
const { title, description } = getErrorCopy(error, { context: "bounty.create" });
toast.error(title, { description });
},
});
};
return (
<div className="min-h-screen bg-background">
<Navbar />
<BountiesHeader
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
isAuthenticated={isAuthenticated}
isCreateOpen={isCreateOpen}
setIsCreateOpen={setIsCreateOpen}
newBounty={newBounty}
setNewBounty={setNewBounty}
onCreate={handleCreateBounty}
isCreating={createBountyMutation.isPending}
/>
{/* Content */}
<section className="pb-20">
<div className="container">
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : filteredBounties.length === 0 ? (
<Card className="card-elegant">
<CardContent className="py-16 text-center">
<Trophy className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2"></h3>
<p className="text-muted-foreground mb-6">
{statusFilter === "all"
? "还没有人发布悬赏,成为第一个发布者吧!"
: "该状态下暂无悬赏"}
</p>
{isAuthenticated && (
<Button onClick={() => setIsCreateOpen(true)} className="gap-2">
<Plus className="w-4 h-4" />
</Button>
)}
</CardContent>
</Card>
) : (
<BountiesGrid bounties={filteredBounties} statusMap={statusMap} />
)}
</div>
</section>
{/* Footer */}
<footer className="py-12 border-t border-border">
<div className="container">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-primary-foreground" />
</div>
<span className="font-semibold"></span>
</div>
<p className="text-sm text-muted-foreground">
© 2026 . All rights reserved.
</p>
</div>
</div>
</footer>
</div>
);
}