feat(page): add page

This commit is contained in:
2026-02-10 15:30:13 +08:00
parent c32e491748
commit 1444cbc665
34 changed files with 2354 additions and 78 deletions

View File

@@ -0,0 +1,139 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/services/api";
import { Media, PaginatedResponse } from "@/services/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Upload, Trash2, Copy } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import Image from "next/image";
import { format } from "date-fns";
import { Card, CardContent } from "@/components/ui/card";
export default function MediaPage() {
const [uploading, setUploading] = useState(false);
const queryClient = useQueryClient();
const { toast } = useToast();
const { data, isLoading } = useQuery({
queryKey: ["media"],
queryFn: async () => {
const res = await api.get<PaginatedResponse<Media>>("/media");
return res.data;
},
});
const uploadMutation = useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append("file", file);
// Assuming API endpoint is /media/upload or similar, checking API docs
// API.md might specify. Assuming standard POST /media with multipart
return api.post("/media", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["media"] });
toast({ title: "上传成功" });
setUploading(false);
},
onError: () => {
setUploading(false);
toast({ title: "上传失败", variant: "destructive" });
}
});
const deleteMutation = useMutation({
mutationFn: (id: string) => api.delete(`/media/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["media"] });
toast({ title: "删除成功" });
},
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setUploading(true);
uploadMutation.mutate(e.target.files[0]);
}
};
const handleCopyUrl = (url: string) => {
navigator.clipboard.writeText(url);
toast({ title: "链接已复制" });
};
const handleDelete = (id: string) => {
if (confirm("确定要删除吗?")) {
deleteMutation.mutate(id);
}
};
if (isLoading) {
return <div className="p-8 text-center text-muted-foreground">...</div>;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight"></h1>
<div className="relative">
<Input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileChange}
accept="image/*"
disabled={uploading}
/>
<Button disabled={uploading}>
<Upload className="mr-2 h-4 w-4" />
{uploading ? "上传中..." : "上传图片"}
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{data?.data.map((item) => (
<Card key={item.id} className="overflow-hidden group">
<CardContent className="p-0 relative aspect-square">
<Image
src={item.url}
alt={item.id}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 16vw"
/>
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
variant="secondary"
size="icon"
onClick={() => handleCopyUrl(item.url)}
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => handleDelete(item.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
<div className="p-2 text-xs text-muted-foreground truncate">
{format(new Date(item.created_at), "yyyy-MM-dd")}
</div>
</Card>
))}
{data?.data.length === 0 && (
<div className="col-span-full text-center py-10 text-muted-foreground">
</div>
)}
</div>
</div>
);
}