Kaynağa Gözat

✨ feat(admin): add file selector components

- 创建FileSelector组件,实现文件选择功能,支持文件上传和列表选择
- 创建SelectedFilePreview组件,用于预览已选择的文件信息
- 重构HomeIcons页面,使用新的文件选择组件替代原有实现
- 优化文件选择流程,合并上传和选择功能,提升用户体验
yourname 7 ay önce
ebeveyn
işleme
4088a535a7

+ 136 - 0
src/client/admin/components/FileSelector.tsx

@@ -0,0 +1,136 @@
+import React from 'react';
+import { Modal, Button, Image, message } from 'antd';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { fileClient } from '@/client/api';
+import type { File as FileType } from '@/server/modules/files/file.entity';
+import MinioUploader from '@/client/admin/components/MinioUploader';
+
+interface FileSelectorProps {
+  visible: boolean;
+  onCancel: () => void;
+  onSelect: (file: FileType) => void;
+  accept?: string;
+  maxSize?: number;
+  uploadPath?: string;
+  uploadButtonText?: string;
+}
+
+const FileSelector: React.FC<FileSelectorProps> = ({
+  visible,
+  onCancel,
+  onSelect,
+  accept = 'image/*',
+  maxSize = 5,
+  uploadPath = '/uploads',
+  uploadButtonText = '上传新文件'
+}) => {
+  const queryClient = useQueryClient();
+  
+  // 获取文件列表
+  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
+  });
+
+  const handleSelectFile = (file: any) => {
+    onSelect(file);
+    message.success(`已选择文件: ${file.name}`);
+  };
+
+  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
+    message.success('文件上传成功!请从列表中选择新上传的文件');
+    // 刷新文件列表
+    queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
+  };
+
+  const filteredFiles = filesData?.data?.filter((f: any) =>
+    !accept || f.type?.startsWith(accept.replace('*', ''))
+  ) || [];
+
+  return (
+    <Modal
+      title="选择文件"
+      open={visible}
+      onCancel={onCancel}
+      width={800}
+      footer={null}
+    >
+      <div style={{ marginBottom: 16 }}>
+        <MinioUploader
+          uploadPath={uploadPath}
+          accept={accept}
+          maxSize={maxSize}
+          onUploadSuccess={handleUploadSuccess}
+          buttonText={uploadButtonText}
+        />
+        <span style={{ color: '#666', fontSize: '12px', marginLeft: 8 }}>
+          上传后请从下方列表中选择文件
+        </span>
+      </div>
+
+      <div style={{ maxHeight: 400, overflowY: 'auto' }}>
+        {isLoading ? (
+          <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
+            加载中...
+          </div>
+        ) : filteredFiles.length > 0 ? (
+          filteredFiles.map((file: any) => (
+            <div
+              key={file.id}
+              style={{
+                display: 'flex',
+                alignItems: 'center',
+                padding: '12px',
+                border: '1px solid #f0f0f0',
+                borderRadius: '4px',
+                marginBottom: '8px',
+                cursor: 'pointer',
+                transition: 'all 0.3s'
+              }}
+              onClick={() => handleSelectFile(file)}
+              onMouseEnter={(e) => {
+                e.currentTarget.style.backgroundColor = '#f6ffed';
+                e.currentTarget.style.borderColor = '#b7eb8f';
+              }}
+              onMouseLeave={(e) => {
+                e.currentTarget.style.backgroundColor = '';
+                e.currentTarget.style.borderColor = '#f0f0f0';
+              }}
+            >
+              <Image
+                src={file.path}
+                alt={file.name}
+                style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
+                preview={false}
+              />
+              <div style={{ flex: 1 }}>
+                <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>{file.name}</div>
+                <div style={{ color: '#666', fontSize: '12px' }}>
+                  ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
+                </div>
+              </div>
+              <Button type="primary" size="small">选择</Button>
+            </div>
+          ))
+        ) : (
+          <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
+            暂无符合条件的文件,请先上传
+          </div>
+        )}
+      </div>
+    </Modal>
+  );
+};
+
+export default FileSelector;

+ 51 - 0
src/client/admin/components/SelectedFilePreview.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Image } from 'antd';
+import { fileClient } from '@/client/api';
+
+interface SelectedFilePreviewProps {
+  fileId: number;
+}
+
+const SelectedFilePreview: React.FC<SelectedFilePreviewProps> = ({ fileId }) => {
+  const { data: fileData } = useQuery({
+    queryKey: ['file-detail', fileId] as const,
+    queryFn: async () => {
+      const response = await fileClient[":id"].$get({
+        param: { id: fileId.toString() }
+      });
+      if (response.status !== 200) throw new Error('获取文件详情失败');
+      return response.json();
+    },
+    enabled: !!fileId
+  });
+
+  const file = fileData?.data;
+
+  if (!file) return null;
+
+  return (
+    <div style={{
+      padding: '12px',
+      backgroundColor: '#f6ffed',
+      border: '1px solid #b7eb8f',
+      borderRadius: '4px',
+      marginBottom: '16px',
+      display: 'flex',
+      alignItems: 'center',
+      gap: '12px'
+    }}>
+      <Image
+        src={file.path}
+        alt={file.name}
+        style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
+      />
+      <div>
+        <div style={{ fontWeight: 'bold' }}>{file.name}</div>
+        <div style={{ color: '#666', fontSize: '12px' }}>ID: {file.id}</div>
+      </div>
+    </div>
+  );
+};
+
+export default SelectedFilePreview;

+ 17 - 121
src/client/admin/pages/HomeIcons.tsx

@@ -1,12 +1,13 @@
-import React, { useState, useEffect } from 'react';
-import { Table, Card, Tabs, Button, Space, Tag, Switch, Popconfirm, message, Modal, Form, Input, Select, Upload, Image } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, LinkOutlined, UploadOutlined } from '@ant-design/icons';
+import React, { useState } from 'react';
+import { Table, Card, Tabs, Button, Space, Tag, Switch, Popconfirm, message, Modal, Form, Input, Image } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, LinkOutlined } from '@ant-design/icons';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { homeIconClient, fileClient } from '@/client/api';
+import { homeIconClient } from '@/client/api';
 import type { HomeIcon } from '@/server/modules/home/home-icon.entity';
 import type { File as FileType } from '@/server/modules/files/file.entity';
 import type { InferResponseType, InferRequestType } from 'hono/client';
-import MinioUploader from '@/client/admin/components/MinioUploader';
+import FileSelector from '@/client/admin/components/FileSelector';
+import SelectedFilePreview from '@/client/admin/components/SelectedFilePreview';
 
 const { TabPane } = Tabs;
 const { TextArea } = Input;
@@ -158,24 +159,7 @@ const HomeIconsPage: React.FC = () => {
   };
 
   const [fileModalVisible, setFileModalVisible] = useState(false);
-  
-  // 获取文件列表
-  const { data: filesData } = useQuery({
-    queryKey: ['files-for-selection'],
-    queryFn: async () => {
-      const response = await fileClient.$get({
-        query: { page: 1, pageSize: 50, keyword: 'image' }
-      });
-      if (response.status !== 200) throw new Error('获取文件列表失败');
-      return response.json();
-    }
-  });
 
-  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
-    message.success('文件上传成功!请在文件管理页面查看文件ID,然后选择文件');
-    // 刷新文件列表
-    queryClient.invalidateQueries({ queryKey: ['files-for-selection'] });
-  };
 
   const handleSelectFile = (file: FileType) => {
     form.setFieldsValue({ fileId: file.id });
@@ -369,29 +353,10 @@ const HomeIconsPage: React.FC = () => {
           >
             {({ getFieldValue }) => {
               const fileId = getFieldValue('fileId');
-              const selectedFile = filesData?.data?.find((f: FileType) => f.id === fileId);
-              
-              return fileId && selectedFile ? (
-                <div style={{
-                  padding: '12px',
-                  backgroundColor: '#f6ffed',
-                  border: '1px solid #b7eb8f',
-                  borderRadius: '4px',
-                  marginBottom: '16px',
-                  display: 'flex',
-                  alignItems: 'center',
-                  gap: '12px'
-                }}>
-                  <Image
-                    src={selectedFile.path}
-                    alt={selectedFile.name}
-                    style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
-                  />
-                  <div>
-                    <div style={{ fontWeight: 'bold' }}>{selectedFile.name}</div>
-                    <div style={{ color: '#666', fontSize: '12px' }}>ID: {selectedFile.id}</div>
-                  </div>
-                </div>
+              // 这里需要重新获取文件信息,或者通过父组件传入选中的文件
+              // 由于我们抽离了组件,这里需要创建一个获取文件详情的query
+              return fileId ? (
+                <SelectedFilePreview fileId={fileId} />
               ) : (
                 <div style={{
                   padding: '8px 12px',
@@ -438,86 +403,17 @@ const HomeIconsPage: React.FC = () => {
             <Switch checkedChildren="启用" unCheckedChildren="禁用" />
           </Form.Item>
 
-          <Form.Item label="文件上传">
-            <MinioUploader
-              uploadPath={`/home-icons/${activeTab}`}
-              accept="image/*"
-              maxSize={5}
-              onUploadSuccess={handleUploadSuccess}
-              buttonText={`上传${activeTab === 'banner' ? '轮播图' : '分类图标'}`}
-            />
-          </Form.Item>
         </Form>
       </Modal>
 
-      {/* 文件选择模态框 */}
-      <Modal
-        title="选择图标文件"
-        open={fileModalVisible}
+      <FileSelector
+        visible={fileModalVisible}
         onCancel={() => setFileModalVisible(false)}
-        width={800}
-        footer={null}
-      >
-        <div style={{ marginBottom: 16 }}>
-          <Button
-            type="primary"
-            icon={<UploadOutlined />}
-            onClick={() => setFileModalVisible(false)}
-            style={{ marginRight: 8 }}
-          >
-            上传新文件
-          </Button>
-          <span style={{ color: '#666', fontSize: '12px' }}>
-            提示:先上传文件,然后从列表中选择
-          </span>
-        </div>
-
-        <div style={{ maxHeight: 400, overflowY: 'auto' }}>
-          {filesData?.data?.filter((f: FileType) => f.type?.startsWith('image/')).map((file: FileType) => (
-            <div
-              key={file.id}
-              style={{
-                display: 'flex',
-                alignItems: 'center',
-                padding: '12px',
-                border: '1px solid #f0f0f0',
-                borderRadius: '4px',
-                marginBottom: '8px',
-                cursor: 'pointer',
-                transition: 'all 0.3s'
-              }}
-              onClick={() => handleSelectFile(file)}
-              onMouseEnter={(e) => {
-                e.currentTarget.style.backgroundColor = '#f6ffed';
-                e.currentTarget.style.borderColor = '#b7eb8f';
-              }}
-              onMouseLeave={(e) => {
-                e.currentTarget.style.backgroundColor = '';
-                e.currentTarget.style.borderColor = '#f0f0f0';
-              }}
-            >
-              <Image
-                src={file.path}
-                alt={file.name}
-                style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
-              />
-              <div style={{ flex: 1 }}>
-                <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>{file.name}</div>
-                <div style={{ color: '#666', fontSize: '12px' }}>
-                  ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
-                </div>
-              </div>
-              <Button type="primary" size="small">选择</Button>
-            </div>
-          ))}
-        </div>
-
-        {(!filesData?.data || filesData.data.filter((f: FileType) => f.type?.startsWith('image/')).length === 0) && (
-          <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
-            暂无图片文件,请先上传
-          </div>
-        )}
-      </Modal>
+        onSelect={handleSelectFile}
+        accept="image/*"
+        uploadPath={`/home-icons/${activeTab}`}
+        uploadButtonText={`上传${activeTab === 'banner' ? '轮播图' : '分类图标'}`}
+      />
     </div>
   );
 };