Selaa lähdekoodia

🗑️ chore(components): 删除未使用的文件预览和选择组件

- 删除FilePreview.tsx组件及其相关类型定义
- 删除FileSelector.tsx组件及其相关类型定义
- 清理不再需要的文件上传和预览功能代码
yourname 4 kuukautta sitten
vanhempi
sitoutus
9b991984a6

+ 0 - 288
src/client/admin-shadcn/components/FilePreview.tsx

@@ -1,288 +0,0 @@
-import React from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { fileClient } from '@/client/api';
-import type { InferResponseType } from 'hono/client';
-import { Eye, FileText, Image as ImageIcon } from 'lucide-react';
-import { Skeleton } from '@/client/components/ui/skeleton';
-import { Card, CardContent } from '@/client/components/ui/card';
-import { Badge } from '@/client/components/ui/badge';
-import { Button } from '@/client/components/ui/button';
-import { toast } from 'sonner';
-
-// 定义文件类型
-type FileItem = InferResponseType<typeof fileClient[':id']['$get'], 200>;
-
-interface FilePreviewItemProps {
-  file: FileItem;
-  size: 'small' | 'medium' | 'large';
-  index?: number;
-  total?: number;
-  onClick?: (file: FileItem) => void;
-}
-
-const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ 
-  file, 
-  size, 
-  index, 
-  total, 
-  onClick 
-}) => {
-  const getSize = () => {
-    switch (size) {
-      case 'small':
-        return { width: 45, height: 45 };
-      case 'medium':
-        return { width: 80, height: 80 };
-      case 'large':
-        return { width: 120, height: 120 };
-      default:
-        return { width: 80, height: 80 };
-    }
-  };
-
-  const { width, height } = getSize();
-  const isImage = file.type?.startsWith('image/');
-  const isVideo = file.type?.startsWith('video/');
-
-  const handlePreview = () => {
-    if (onClick) {
-      onClick(file);
-    } else if (isImage || isVideo) {
-      window.open(file.fullUrl, '_blank');
-    } else {
-      toast.warning('该文件类型不支持预览');
-    }
-  };
-
-  // 获取文件图标
-  const getFileIcon = (type?: string) => {
-    if (!type) return <FileText className="h-8 w-8 text-gray-400" />;
-    
-    if (type.startsWith('image/')) {
-      return <ImageIcon className="h-8 w-8 text-blue-500" />;
-    } else if (type.startsWith('video/')) {
-      return <FileText className="h-8 w-8 text-red-500" />;
-    } else if (type.startsWith('audio/')) {
-      return <FileText className="h-8 w-8 text-purple-500" />;
-    } else if (type.includes('pdf')) {
-      return <FileText className="h-8 w-8 text-red-500" />;
-    } else if (type.includes('word')) {
-      return <FileText className="h-8 w-8 text-blue-600" />;
-    } else if (type.includes('excel') || type.includes('sheet')) {
-      return <FileText className="h-8 w-8 text-green-500" />;
-    } else {
-      return <FileText className="h-8 w-8 text-gray-500" />;
-    }
-  };
-
-  // 获取文件类型标签
-  const getFileTypeBadge = (type: string) => {
-    if (type.startsWith('image/')) {
-      return { text: '图片', color: 'bg-blue-100 text-blue-800' };
-    } else if (type.startsWith('video/')) {
-      return { text: '视频', color: 'bg-red-100 text-red-800' };
-    } else if (type.startsWith('audio/')) {
-      return { text: '音频', color: 'bg-purple-100 text-purple-800' };
-    } else if (type.includes('pdf')) {
-      return { text: 'PDF', color: 'bg-red-100 text-red-800' };
-    } else if (type.includes('word')) {
-      return { text: '文档', color: 'bg-blue-100 text-blue-800' };
-    } else if (type.includes('excel') || type.includes('sheet')) {
-      return { text: '表格', color: 'bg-green-100 text-green-800' };
-    } else {
-      return { text: '文件', color: 'bg-gray-100 text-gray-800' };
-    }
-  };
-
-  return (
-    <div
-      className="relative group cursor-pointer"
-      style={{ width, height }}
-      onClick={handlePreview}
-    >
-      {/* 文件预览容器 */}
-      <div className={`
-        relative overflow-hidden rounded-lg border transition-all duration-200
-        ${isImage ? 'border-gray-200' : 'border-gray-300 bg-gray-50'}
-        group-hover:shadow-md group-hover:border-primary
-      `}>
-        {isImage ? (
-          // 图片预览
-          <img
-            src={file.fullUrl}
-            alt={file.name}
-            className="w-full h-full object-cover"
-            loading="lazy"
-          />
-        ) : (
-          // 非图片文件预览
-          <div className="w-full h-full flex flex-col items-center justify-center">
-            {getFileIcon(file.type)}
-            <span className="text-xs text-center mt-1 px-1 truncate max-w-full">
-              {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
-            </span>
-          </div>
-        )}
-
-        {/* 悬停遮罩 */}
-        <div className={`
-          absolute inset-0 bg-black/60 flex flex-col items-center justify-center
-          opacity-0 group-hover:opacity-100 transition-opacity duration-200
-          text-white text-xs
-        `}>
-          <Eye className="h-4 w-4 mb-1" />
-          <span>{isImage || isVideo ? '预览' : '查看'}</span>
-        </div>
-
-        {/* 序号标记 */}
-        {index !== undefined && total !== undefined && total > 1 && (
-          <div className={`
-            absolute top-1 right-1 bg-black/70 text-white text-xs
-            px-1.5 py-0.5 rounded
-          `}>
-            {index + 1}
-          </div>
-        )}
-      </div>
-
-      {/* 文件类型标签 */}
-      <Badge 
-        className={`
-          absolute bottom-1 left-1 text-xs px-1 py-0
-          ${getFileTypeBadge(file.type).color}
-        `}
-      >
-        {getFileTypeBadge(file.type).text}
-      </Badge>
-    </div>
-  );
-};
-
-interface FilePreviewProps {
-  fileIds?: number[];
-  files?: any[];
-  maxCount?: number;
-  size?: 'small' | 'medium' | 'large';
-  showCount?: boolean;
-  onFileClick?: (file: FileItem) => void;
-  className?: string;
-}
-
-const FilePreview: React.FC<FilePreviewProps> = ({
-  fileIds = [],
-  files = [],
-  maxCount = 6,
-  size = 'medium',
-  showCount = true,
-  onFileClick,
-  className = '',
-}) => {
-  // 合并文件ID和文件对象
-  const allFileIds = [...fileIds, ...(files?.map(f => f.id) || [])];
-  const uniqueFileIds = [...new Set(allFileIds)].filter(Boolean);
-
-  // 使用 React Query 查询文件详情
-  const { data: fileDetails, isLoading, error } = useQuery({
-    queryKey: ['files', uniqueFileIds],
-    queryFn: async () => {
-      if (uniqueFileIds.length === 0) return [];
-      
-      const promises = uniqueFileIds.map(async (id) => {
-        try {
-          const response = await fileClient[':id']['$get']({ param: { id: id.toString() } });
-          if (response.ok) {
-            return response.json();
-          }
-          return null;
-        } catch (error) {
-          console.error(`获取文件 ${id} 详情失败:`, error);
-          return null;
-        }
-      });
-      
-      const results = await Promise.all(promises);
-      return results.filter(Boolean) as FileItem[];
-    },
-    enabled: uniqueFileIds.length > 0,
-    staleTime: 5 * 60 * 1000, // 5分钟
-    gcTime: 10 * 60 * 1000, // 10分钟
-  });
-
-  // 加载状态
-  if (isLoading) {
-    return (
-      <div className={`flex justify-center py-8 ${className}`}>
-        <div className="space-y-2">
-          <div className="flex gap-2 justify-center">
-            {[...Array(Math.min(maxCount, 3))].map((_, i) => (
-              <Skeleton key={i} className={`rounded-lg ${size === 'small' ? 'w-12 h-12' : size === 'medium' ? 'w-20 h-20' : 'w-24 h-24'}`} />
-            ))}
-          </div>
-          <p className="text-sm text-gray-500 text-center">加载中...</p>
-        </div>
-      </div>
-    );
-  }
-
-  // 错误状态
-  if (error) {
-    return (
-      <div className={`flex flex-col items-center justify-center py-8 ${className}`}>
-        <FileText className="h-12 w-12 text-gray-400 mb-2" />
-        <p className="text-sm text-gray-600">加载图片失败</p>
-        <Button 
-          variant="outline" 
-          size="sm" 
-          className="mt-2"
-          onClick={() => {
-            // 这里可以添加重试逻辑
-            toast.info('请刷新页面重试');
-          }}
-        >
-          重试
-        </Button>
-      </div>
-    );
-  }
-
-  const displayFiles = fileDetails?.slice(0, maxCount) || [];
-  const remainingCount = Math.max(0, (fileDetails?.length || 0) - maxCount);
-
-  // 空状态
-  if (displayFiles.length === 0) {
-    return (
-      <div className={`flex flex-col items-center justify-center py-6 ${className}`}>
-        <FileText className="h-12 w-12 text-gray-400 mb-2" />
-        <p className="text-sm text-gray-600">暂无图片</p>
-      </div>
-    );
-  }
-
-  return (
-    <div className={className}>
-      <div className="flex flex-wrap gap-2 items-start">
-        {displayFiles.map((file, index) => (
-          <FilePreviewItem
-            key={file.id}
-            file={file}
-            size={size}
-            index={index}
-            total={displayFiles.length}
-            onClick={onFileClick}
-          />
-        ))}
-      </div>
-      
-      {/* 剩余数量提示 */}
-      {showCount && remainingCount > 0 && (
-        <div className="mt-2 text-sm text-gray-500">
-          还有 {remainingCount} 个文件未显示
-        </div>
-      )}
-    </div>
-  );
-};
-
-// 导出组件和类型
-export default FilePreview;
-export type { FilePreviewProps };

+ 0 - 312
src/client/admin-shadcn/components/FileSelector.tsx

@@ -1,312 +0,0 @@
-import React, { useState } from 'react';
-import { useQuery, useQueryClient } from '@tanstack/react-query';
-import { Button } from '@/client/components/ui/button';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
-import { Card, CardContent } from '@/client/components/ui/card';
-import { Badge } from '@/client/components/ui/badge';
-import { toast } from 'sonner';
-import { fileClient } from '@/client/api';
-import type { FileType } from '@/server/modules/files/file.schema';
-import MinioUploader from '@/client/admin-shadcn/components/MinioUploader';
-import { Check, Upload } from 'lucide-react';
-
-// 定义重载的 props 类型
-interface SingleFileSelectorProps {
-  visible: boolean;
-  onCancel: () => void;
-  onSelect: (file: FileType) => void;
-  accept?: string;
-  maxSize?: number;
-  uploadPath?: string;
-  uploadButtonText?: string;
-  multiple?: false;
-}
-
-interface MultipleFileSelectorProps {
-  visible: boolean;
-  onCancel: () => void;
-  onSelect: (files: FileType[]) => void;
-  accept?: string;
-  maxSize?: number;
-  uploadPath?: string;
-  uploadButtonText?: string;
-  multiple: true;
-}
-
-type FileSelectorProps = SingleFileSelectorProps | MultipleFileSelectorProps;
-
-const FileSelector: React.FC<FileSelectorProps> = ({
-  visible,
-  onCancel,
-  onSelect,
-  accept = 'image/*',
-  maxSize = 5,
-  uploadPath = '/uploads',
-  uploadButtonText = '上传新文件',
-  multiple = false,
-  ...props
-}) => {
-  const queryClient = useQueryClient();
-  const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]);
-  
-  // 获取文件列表
-  const { data: filesData, isLoading } = useQuery({
-    queryKey: ['files-for-selection', accept] as const,
-    queryFn: async () => {
-      const response = await fileClient.$get({
-        query: {
-          page: 1,
-          pageSize: 50,
-          keyword: accept.startsWith('image/') ? 'image' : undefined
-        }
-      });
-      if (response.status !== 200) throw new Error('获取文件列表失败');
-      return response.json();
-    },
-    enabled: visible
-  });
-
-  // 重置选择状态
-  React.useEffect(() => {
-    if (!visible) {
-      setSelectedFiles([]);
-    }
-  }, [visible]);
-
-  const handleSelectFile = (file: FileType) => {
-    if (!file) {
-      console.error('No file provided to handleSelectFile');
-      return;
-    }
-
-    if (multiple) {
-      // 多选模式
-      const newSelection = selectedFiles.some(f => f.id === file.id)
-        ? selectedFiles.filter(f => f.id !== file.id)
-        : [...selectedFiles, file];
-      
-      setSelectedFiles(newSelection);
-    } else {
-      // 单选模式
-      setSelectedFiles([file]);
-    }
-  };
-
-  const handleConfirm = () => {
-    if (selectedFiles.length === 0) {
-      toast.warning('请先选择文件');
-      return;
-    }
-
-    if (multiple) {
-      // 多选模式
-      (onSelect as MultipleFileSelectorProps['onSelect'])(selectedFiles);
-    } else {
-      // 单选模式
-      (onSelect as SingleFileSelectorProps['onSelect'])(selectedFiles[0]);
-    }
-    
-    onCancel();
-  };
-
-  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
-    toast.success('文件上传成功!请从列表中选择新上传的文件');
-    // 刷新文件列表
-    queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
-  };
-
-  const filteredFiles = Array.isArray(filesData?.data)
-    ? filesData.data.filter((f: any) => !accept || f?.type?.startsWith(accept.replace('*', '')))
-    : [];
-
-  const isFileSelected = (file: FileType) => {
-    return selectedFiles.some(f => f.id === file.id);
-  };
-
-  const getSelectionText = () => {
-    if (multiple) {
-      return selectedFiles.length > 0 ? `已选择 ${selectedFiles.length} 个文件` : '请选择文件';
-    }
-    return selectedFiles.length > 0 ? `已选择: ${selectedFiles[0].name}` : '请选择文件';
-  };
-
-  // 格式化文件大小
-  const formatFileSize = (bytes: number) => {
-    if (bytes === 0) return '0 Bytes';
-    const k = 1024;
-    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-  };
-
-  // 获取文件类型的显示标签
-  const getFileTypeBadge = (type: string) => {
-    if (type.startsWith('image/')) {
-      return { text: '图片', color: 'bg-blue-100 text-blue-800' };
-    } else if (type.startsWith('video/')) {
-      return { text: '视频', color: 'bg-red-100 text-red-800' };
-    } else if (type.startsWith('audio/')) {
-      return { text: '音频', color: 'bg-purple-100 text-purple-800' };
-    } else if (type.includes('pdf')) {
-      return { text: 'PDF', color: 'bg-red-100 text-red-800' };
-    } else if (type.includes('word')) {
-      return { text: '文档', color: 'bg-blue-100 text-blue-800' };
-    } else if (type.includes('excel') || type.includes('sheet')) {
-      return { text: '表格', color: 'bg-green-100 text-green-800' };
-    } else {
-      return { text: '文件', color: 'bg-gray-100 text-gray-800' };
-    }
-  };
-
-  return (
-    <Dialog open={visible} onOpenChange={onCancel}>
-      <DialogContent className="max-w-4xl max-h-[80vh]">
-        <DialogHeader>
-          <DialogTitle>选择文件</DialogTitle>
-          <DialogDescription>
-            从已有文件中选择,或上传新文件
-          </DialogDescription>
-        </DialogHeader>
-        
-        <div className="space-y-4">
-          {/* 上传区域 */}
-          <Card>
-            <CardContent className="pt-6">
-              <MinioUploader
-                uploadPath={uploadPath}
-                accept={accept}
-                maxSize={maxSize}
-                onUploadSuccess={handleUploadSuccess}
-                buttonText={uploadButtonText}
-              />
-              <p className="text-sm text-gray-500 mt-2">
-                上传后请从下方列表中选择文件
-              </p>
-            </CardContent>
-          </Card>
-
-          {/* 选择状态显示 */}
-          <Card>
-            <CardContent className="pt-6">
-              <div className="bg-green-50 border border-green-200 rounded-md p-3">
-                <p className="text-sm text-green-700">{getSelectionText()}</p>
-              </div>
-            </CardContent>
-          </Card>
-
-          {/* 文件列表 */}
-          <div className="space-y-2 max-h-96 overflow-y-auto">
-            {isLoading ? (
-              <Card>
-                <CardContent className="text-center py-8">
-                  <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
-                  <p className="text-gray-500 mt-2">加载中...</p>
-                </CardContent>
-              </Card>
-            ) : filteredFiles.length > 0 ? (
-              <div className="grid gap-3">
-                {filteredFiles.map((file) => {
-                  const typeBadge = getFileTypeBadge(file.type);
-                  const isSelected = isFileSelected(file);
-                  
-                  return (
-                    <Card
-                      key={file.id}
-                      className={`cursor-pointer transition-all hover:shadow-md ${
-                        isSelected ? 'ring-2 ring-primary' : ''
-                      }`}
-                      onClick={() => handleSelectFile(file)}
-                    >
-                      <CardContent className="p-4">
-                        <div className="flex items-center space-x-4">
-                          {/* 文件预览图 */}
-                          <div className="relative">
-                            {file.type.startsWith('image/') ? (
-                              <img
-                                src={file.fullUrl}
-                                alt={file.name}
-                                className="w-16 h-16 object-cover rounded-md"
-                              />
-                            ) : (
-                              <div className="w-16 h-16 bg-gray-100 rounded-md flex items-center justify-center">
-                                <Upload className="h-8 w-8 text-gray-400" />
-                              </div>
-                            )}
-                            {isSelected && (
-                              <div className="absolute -top-2 -right-2 bg-primary text-white rounded-full p-1">
-                                <Check className="h-3 w-3" />
-                              </div>
-                            )}
-                          </div>
-
-                          {/* 文件信息 */}
-                          <div className="flex-1 min-w-0">
-                            <h4 className="text-sm font-medium truncate">{file.name}</h4>
-                            <div className="flex items-center space-x-2 mt-1">
-                              <Badge className={`${typeBadge.color} text-xs`}>
-                                {typeBadge.text}
-                              </Badge>
-                              <span className="text-xs text-gray-500">ID: {file.id}</span>
-                              <span className="text-xs text-gray-500">
-                                {formatFileSize(file.size || 0)}
-                              </span>
-                            </div>
-                          </div>
-
-                          {/* 选择按钮 */}
-                          <Button
-                            type="button"
-                            variant={isSelected ? "default" : "outline"}
-                            size="sm"
-                            onClick={(e) => {
-                              e.stopPropagation();
-                              handleSelectFile(file);
-                            }}
-                          >
-                            {isSelected ? '已选择' : '选择'}
-                          </Button>
-                        </div>
-                      </CardContent>
-                    </Card>
-                  );
-                })}
-              </div>
-            ) : (
-              <Card>
-                <CardContent className="text-center py-8">
-                  <Upload className="h-12 w-12 mx-auto text-gray-400 mb-4" />
-                  <p className="text-gray-600">暂无符合条件的文件</p>
-                  <p className="text-sm text-gray-500 mt-2">请先上传文件或调整筛选条件</p>
-                </CardContent>
-              </Card>
-            )}
-          </div>
-        </div>
-
-        <DialogFooter>
-          <Button type="button" variant="outline" onClick={onCancel}>
-            取消
-          </Button>
-          <Button 
-            type="button" 
-            onClick={handleConfirm}
-            disabled={selectedFiles.length === 0}
-          >
-            确认选择
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
-  );
-};
-
-// 类型守卫函数
-function isMultipleSelector(props: FileSelectorProps): props is MultipleFileSelectorProps {
-  return props.multiple === true;
-}
-
-function isSingleSelector(props: FileSelectorProps): props is SingleFileSelectorProps {
-  return props.multiple === false || props.multiple === undefined;
-}
-
-export default FileSelector;