| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import React, { useState, useEffect } from 'react';
- import { publicTemplateClient } from '@/client/api';
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
- import { Button } from '@/client/components/ui/button';
- import { Badge } from '@/client/components/ui/badge';
- import { Skeleton } from '@/client/components/ui/skeleton';
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
- import { Input } from '@/client/components/ui/input';
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/client/components/ui/tabs';
- import { Eye, Download, Star, Search } from 'lucide-react';
- import { useAuth } from '@/client/home/hooks/AuthProvider';
- import { toast } from 'sonner';
- import { useNavigate } from 'react-router-dom';
- interface Template {
- id: number;
- title: string;
- description: string | null;
- category: string;
- isFree: number;
- downloadCount: number;
- previewUrl?: string;
- file: {
- id: number;
- name: string;
- fullUrl: string;
- type: string | null;
- size: number | null;
- } | null;
- }
- const TemplateSquare: React.FC = () => {
- const [templates, setTemplates] = useState<Template[]>([]);
- const [categories, setCategories] = useState<string[]>([]);
- const [loading, setLoading] = useState(true);
- const [category, setCategory] = useState<string>('all');
- const [isFree, setIsFree] = useState<string>('all');
- const [searchKeyword, setSearchKeyword] = useState('');
- const [pagination, setPagination] = useState({
- current: 1,
- pageSize: 12,
- total: 0
- });
-
- const { user } = useAuth();
- const navigate = useNavigate();
- useEffect(() => {
- fetchTemplates();
- fetchCategories();
- }, [category, isFree, pagination.current]);
- const fetchTemplates = async () => {
- try {
- setLoading(true);
- const response = await publicTemplateClient.$get({
- query: {
- page: pagination.current,
- pageSize: pagination.pageSize,
- category: category !== 'all' ? category : undefined,
- isFree: isFree !== 'all' ? parseInt(isFree) : undefined
- }
- });
-
- if (response.ok) {
- const data = await response.json();
- setTemplates(data.data);
- setPagination(prev => ({ ...prev, total: data.pagination.total }));
- }
- } catch (error) {
- console.error('Failed to fetch templates:', error);
- toast.error('获取模板列表失败');
- } finally {
- setLoading(false);
- }
- };
- const fetchCategories = async () => {
- try {
- const response = await publicTemplateClient.categories.$get();
- if (response.ok) {
- const data = await response.json();
- setCategories(data);
- }
- } catch (error) {
- console.error('Failed to fetch categories:', error);
- }
- };
- const handlePreview = async (template: Template) => {
- try {
- const response = await publicTemplateClient[':id'].preview.$get({
- param: { id: template.id.toString() }
- });
-
- if (response.ok) {
- const data = await response.json();
- window.open(data.previewUrl, '_blank');
- } else {
- toast.error('获取预览失败');
- }
- } catch (error) {
- console.error('Failed to preview template:', error);
- toast.error('预览失败');
- }
- };
- const handleDownload = async (template: Template) => {
- if (!user) {
- toast.error('请先登录');
- navigate('/login');
- return;
- }
- if (!user.membership && !template.isFree) {
- toast.error('需要购买会员才能下载');
- navigate('/pricing');
- return;
- }
- try {
- const response = await publicTemplateClient[':id'].download.$post({
- param: { id: template.id.toString() }
- });
-
- if (response.ok) {
- const data = await response.json();
- window.open(data.downloadUrl, '_blank');
- toast.success('下载已开始');
- } else if (response.status === 403) {
- toast.error('需要购买会员才能下载');
- navigate('/pricing');
- } else {
- toast.error('下载失败');
- }
- } catch (error) {
- console.error('Failed to download template:', error);
- toast.error('下载失败');
- }
- };
- const handlePageChange = (page: number) => {
- setPagination(prev => ({ ...prev, current: page }));
- };
- const TemplateCard = ({ template }: { template: Template }) => (
- <Card className="hover:shadow-lg transition-shadow">
- <CardHeader>
- <div className="flex justify-between items-start">
- <CardTitle className="text-lg">{template.title}</CardTitle>
- <Badge variant={template.isFree ? "default" : "secondary"}>
- {template.isFree ? "免费" : "会员"}
- </Badge>
- </div>
- <CardDescription className="line-clamp-2">
- {template.description || '暂无描述'}
- </CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex items-center justify-between text-sm text-gray-600">
- <span className="flex items-center gap-1">
- <Star className="w-4 h-4" />
- <span>下载 {template.downloadCount}</span>
- </span>
- <Badge variant="outline">{template.category}</Badge>
- </div>
- </CardContent>
- <CardFooter className="flex gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={() => handlePreview(template)}
- className="flex-1"
- >
- <Eye className="w-4 h-4 mr-2" />
- 预览
- </Button>
- <Button
- size="sm"
- onClick={() => handleDownload(template)}
- className="flex-1"
- disabled={!template.isFree && !user?.membership}
- >
- <Download className="w-4 h-4 mr-2" />
- 下载
- </Button>
- </CardFooter>
- </Card>
- );
- const LoadingSkeleton = () => (
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
- {[...Array(8)].map((_, i) => (
- <Card key={i}>
- <CardHeader>
- <Skeleton className="h-4 w-3/4" />
- <Skeleton className="h-3 w-full mt-2" />
- <Skeleton className="h-3 w-2/3" />
- </CardHeader>
- <CardContent>
- <Skeleton className="h-3 w-1/2" />
- </CardContent>
- <CardFooter className="flex gap-2">
- <Skeleton className="h-8 flex-1" />
- <Skeleton className="h-8 flex-1" />
- </CardFooter>
- </Card>
- ))}
- </div>
- );
- return (
- <div className="container mx-auto py-8 px-4">
- <div className="mb-8">
- <h1 className="text-3xl font-bold mb-2">模板广场</h1>
- <p className="text-gray-600">精选各类文档模板,助您高效办公</p>
- </div>
- <div className="mb-6">
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
- <div className="md:col-span-2">
- <div className="relative">
- <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
- <Input
- placeholder="搜索模板..."
- value={searchKeyword}
- onChange={(e) => setSearchKeyword(e.target.value)}
- className="pl-10"
- />
- </div>
- </div>
-
- <Select value={category} onValueChange={setCategory}>
- <SelectTrigger>
- <SelectValue placeholder="全部分类" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部分类</SelectItem>
- {categories.map(cat => (
- <SelectItem key={cat} value={cat}>{cat}</SelectItem>
- ))}
- </SelectContent>
- </Select>
- <Select value={isFree} onValueChange={setIsFree}>
- <SelectTrigger>
- <SelectValue placeholder="全部类型" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部类型</SelectItem>
- <SelectItem value="1">免费</SelectItem>
- <SelectItem value="0">会员</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- <Tabs defaultValue="all" className="mb-6">
- <TabsList>
- <TabsTrigger value="all" onClick={() => setIsFree('all')}>全部</TabsTrigger>
- <TabsTrigger value="free" onClick={() => setIsFree('1')}>免费模板</TabsTrigger>
- <TabsTrigger value="vip" onClick={() => setIsFree('0')}>会员专享</TabsTrigger>
- </TabsList>
- </Tabs>
- {loading ? (
- <LoadingSkeleton />
- ) : templates.length === 0 ? (
- <div className="text-center py-12">
- <p className="text-gray-500">暂无模板</p>
- </div>
- ) : (
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
- {templates.map(template => (
- <TemplateCard key={template.id} template={template} />
- ))}
- </div>
- )}
- {pagination.total > pagination.pageSize && (
- <div className="mt-8 flex justify-center">
- <div className="flex gap-2">
- {Array.from({ length: Math.ceil(pagination.total / pagination.pageSize) }, (_, i) => (
- <Button
- key={i}
- variant={pagination.current === i + 1 ? "default" : "outline"}
- size="sm"
- onClick={() => handlePageChange(i + 1)}
- >
- {i + 1}
- </Button>
- ))}
- </div>
- </div>
- )}
- </div>
- );
- };
- export default TemplateSquare;
|