143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import api from "@/services/api";
|
|
import { Media, PagedMedia } 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 () => {
|
|
return api.get<PagedMedia>("/media");
|
|
},
|
|
});
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
const mediaItems = data?.items || [];
|
|
|
|
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">
|
|
{mediaItems.map((item: Media) => (
|
|
<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>
|
|
))}
|
|
{mediaItems.length === 0 && (
|
|
<div className="col-span-full text-center py-10 text-muted-foreground">
|
|
暂无图片
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|