feat(web): share chat & app chat support files

This commit is contained in:
zhaoying
2026-02-06 21:11:51 +08:00
parent 2db583d62d
commit 6849c620b8
34 changed files with 1571 additions and 251 deletions

View File

@@ -0,0 +1,251 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-06 21:09:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-06 21:09:42
*/
/**
* File Upload Component
*
* A reusable file upload component based on Ant Design Upload.
* Supports single/multiple file uploads, drag-and-drop, file validation, and preview.
*
* Features:
* - File type validation (images, documents, etc.)
* - File size validation
* - Auto-upload or manual upload modes
* - Progress tracking
* - Custom upload actions and headers
* - File list management
*
* @component
*/
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Upload, Progress, App } from 'antd';
import type { UploadProps, UploadFile } from 'antd';
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next';
import { cookieUtils } from '@/utils/request'
import { fileUploadUrl } from '@/api/fileStorage'
interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
/** Upload API endpoint */
action?: string;
/** Enable multiple file selection */
multiple?: boolean;
/** List of uploaded files */
fileList?: UploadFile[];
/** Callback when file list changes */
onChange?: (fileList: UploadFile | UploadFile[]) => void;
customRequest?: RcUploadProps['customRequest'];
/** Custom upload request configuration */
requestConfig?: {
data?: Record<string, string | number | boolean>;
headers?: Record<string, string>;
};
/** Disable upload */
disabled?: boolean;
/** File size limit in MB */
fileSize?: number;
/** Allowed file types ['doc', 'xls', 'ppt', 'pdf'] */
fileType?: string[];
/** Auto-upload on file selection, default is true */
isAutoUpload?: boolean;
/** Maximum number of files allowed */
maxCount?: number;
/** Custom file removal callback */
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
/** Trigger to reset file list */
update?: boolean;
}
// Mapping of file extensions to MIME types
const ALL_FILE_TYPE: {
[key: string]: string;
} = {
// txt: 'text/plain',
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
csv: 'text/csv',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
// md: 'text/markdown',
// htm: 'text/html',
// html: 'text/html',
// json: 'application/json',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
bmp: 'image/bmp',
webp: 'image/webp',
svg: 'image/svg+xml',
}
export interface UploadFilesRef {
/** Current file list */
fileList: UploadFile[];
/** Clear all uploaded files */
clearFiles: () => void;
}
/**
* Common upload component based on Ant Design Upload
* Supports single/multiple file uploads, drag-and-drop, file validation, and preview
*/
const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
action = fileUploadUrl,
multiple = false,
fileList: propFileList = [],
onChange,
disabled = false,
fileSize = 5,
fileType = Object.entries(ALL_FILE_TYPE).map(([key]) => key),
isAutoUpload = true,
maxCount = 1,
onRemove: customOnRemove,
update,
...props
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
const [accept, setAccept] = useState<string | undefined>();
// Reset file list when update prop changes
useEffect(() => {
setFileList([])
}, [update])
/**
* Validates file type and size before upload
* @returns Upload.LIST_IGNORE to prevent upload, or true to proceed
*/
const beforeUpload: RcUploadProps['beforeUpload'] = (file) => {
// Validate file size
if (fileSize) {
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
if (!isLtMaxSize) {
message.error(t('common.fileSizeTip', { size: fileSize }));
return Upload.LIST_IGNORE;
}
}
// Validate file type
if (fileType && fileType.length > 0) {
// Get file extension
const fileName = file.name.toLowerCase();
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
// Check if extension is in allowed types list
const isValidExtension = fileType.some(type => type.toLowerCase() === fileExtension);
// Also check MIME type if available (as fallback validation)
const isValidMimeType = file.type && accept ? accept.includes(file.type) : true;
if (!isValidExtension && !isValidMimeType) {
message.error(`${t('common.fileAcceptTip')} ${fileExtension || file.type}`);
return Upload.LIST_IGNORE;
}
}
if (!isAutoUpload) {
const newFileList = [...fileList, file as UploadFile];
setFileList(newFileList);
onChange?.(newFileList);
return Upload.LIST_IGNORE; // Prevent auto-upload
}
return isAutoUpload;
};
/**
* Handles upload state changes
*/
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList, event }) => {
console.log('event', event)
setFileList(newFileList);
if (onChange) {
onChange(maxCount === 1 ? newFileList[0] : newFileList);
}
};
/**
* Clears all uploaded files
*/
const clearFiles = () => {
setFileList([]);
if (onChange) {
onChange([]);
}
}
// Build accept string from file types (includes both MIME types and extensions)
useEffect(() => {
if (fileType && fileType.length > 0) {
// Include both MIME types and file extensions
const acceptArray: string[] = [];
fileType.forEach((type: string) => {
const lowerType = type.toLowerCase();
// Add MIME type (if exists)
const mimeType = ALL_FILE_TYPE[lowerType];
if (mimeType) {
acceptArray.push(mimeType);
}
// Add file extension (.md, .html, etc.)
acceptArray.push(`.${lowerType}`);
});
setAccept(acceptArray.join(','));
} else {
setAccept(undefined);
}
}, [fileType])
// Generate upload component configuration
const uploadProps: UploadProps = {
action,
multiple: multiple && maxCount > 1,
fileList,
beforeUpload,
headers: {
authorization: `Bearer ${cookieUtils.get('authToken')}`,
},
onChange: handleChange,
accept,
disabled,
showUploadList: false,
itemRender: (_, file, __, actions) => {
return (
<div key={file.uid} className="rb:relative rb:w-full rb:pt-2 rb:px-2.5 rb-pb-[10px] rb:border rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-0.5">
{file.name}
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>Cancel</span>
</div>
<Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />
</div>
);
},
className: 'rb:-mb-1.5!',
...props,
};
// Expose methods to parent component via ref
useImperativeHandle(ref, () => ({
fileList,
clearFiles
}));
return (
<Upload
{...uploadProps}
>
{t('memoryConversation.uploadFile')}
</Upload>
);
});
export default UploadFiles;

View File

@@ -0,0 +1,135 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-06 21:09:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-06 21:09:47
*/
/**
* Upload File List Modal Component
*
* A modal dialog for adding remote files via URL.
* Allows users to specify file type and URL for files hosted externally.
*
* Features:
* - Dynamic form fields for multiple file URLs
* - File type selection (currently supports images)
* - Form validation
* - Add/remove file entries
*
* @component
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, Select, Button, Space } from 'antd';
import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import type { UploadFileListModalRef } from '../types'
import RbModal from '@/components/RbModal'
const FormItem = Form.Item;
interface UploadFileListModalProps {
/** Callback to refresh parent component with new file list */
refresh: (fileList?: any[]) => void;
}
/**
* Modal for adding remote files via URL
*/
const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false)
/**
* Closes the modal and resets loading state
*/
const handleClose = () => {
setVisible(false);
setLoading(false)
};
/**
* Opens the modal and resets form fields
*/
const handleOpen = () => {
setVisible(true);
form.resetFields();
};
/**
* Validates and saves the file list
* Transforms form values into file objects with transfer_method: 'remote_url'
*/
const handleSave = () => {
form.validateFields().then((values) => {
const fileList = values.files?.map((file: any) => ({
...file,
uid: Math.random().toString(36).substr(2, 9),
transfer_method: 'remote_url'
})) || [];
refresh(fileList)
handleClose()
})
}
// Expose methods to parent component via ref
useImperativeHandle(ref, () => ({
handleOpen
}));
return (
<RbModal
title={t('memoryConversation.addRemoteFile')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form form={form} layout="vertical">
<Form.List name="files">
{(fields, { add, remove }) => (
<>
{/* Render each file entry with type selector and URL input */}
{fields.map(({ key, name, ...restField }) => (
<Space key={key} style={{ display: 'flex' }} align="baseline">
<FormItem
{...restField}
name={[name, 'type']}
initialValue="image"
>
<Select
placeholder={t('memoryConversation.fileType')}
options={[
{ label: t('memoryConversation.image'), value: 'image' }
]}
className="rb:w-30"
/>
</FormItem>
<FormItem
{...restField}
name={[name, 'url']}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('memoryConversation.fileUrl')} className="rb:w-82.5" />
</FormItem>
<MinusCircleOutlined onClick={() => remove(name)} style={{ marginTop: 30 }} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
{t('common.add')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</RbModal>
);
});
export default UploadFileListModal;