feat(page): add page
This commit is contained in:
139
src/app/(dashboard)/media/page.tsx
Normal file
139
src/app/(dashboard)/media/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user