feat(web-client): add per-type file size validation on upload
Enforce Claude API file size limits at upload time with user-friendly error messages: - Images: max 5MB (Claude API hard limit) - PDF: max 25MB (32MB request limit minus headroom) - Other documents: max 50MB (general upload limit) Replaced duplicate ALLOWED_TYPES/MAX_FILE_SIZE in InputArea with shared validateFile() from fileService, showing alert() on rejection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5338bdfc0f
commit
470ec9a64e
|
|
@ -2,6 +2,7 @@ import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent, ClipboardEvent
|
||||||
import { Send, Paperclip, X, Image, FileText, Loader2, Upload } from 'lucide-react';
|
import { Send, Paperclip, X, Image, FileText, Loader2, Upload } from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { FileAttachment } from '../stores/chatStore';
|
import { FileAttachment } from '../stores/chatStore';
|
||||||
|
import { validateFile, ALLOWED_FILE_TYPES } from '@/shared/services/fileService';
|
||||||
|
|
||||||
interface PendingFile {
|
interface PendingFile {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -20,25 +21,6 @@ interface InputAreaProps {
|
||||||
uploadProgress?: Record<string, { progress: number; status: string }>;
|
uploadProgress?: Record<string, { progress: number; status: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 允许上传的文件类型
|
|
||||||
const ALLOWED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/gif',
|
|
||||||
'image/webp',
|
|
||||||
'image/svg+xml',
|
|
||||||
'application/pdf',
|
|
||||||
'application/msword',
|
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
||||||
'application/vnd.ms-excel',
|
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
'text/plain',
|
|
||||||
'text/csv',
|
|
||||||
'text/markdown',
|
|
||||||
];
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
||||||
|
|
||||||
export function InputArea({
|
export function InputArea({
|
||||||
onSend,
|
onSend,
|
||||||
disabled,
|
disabled,
|
||||||
|
|
@ -89,12 +71,9 @@ export function InputArea({
|
||||||
|
|
||||||
const validFiles: File[] = [];
|
const validFiles: File[] = [];
|
||||||
Array.from(files).forEach((file) => {
|
Array.from(files).forEach((file) => {
|
||||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
const result = validateFile(file);
|
||||||
console.warn(`不支持的文件类型: ${file.type}`);
|
if (!result.valid) {
|
||||||
return;
|
alert(result.error);
|
||||||
}
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
console.warn(`文件过大: ${file.name}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
validFiles.push(file);
|
validFiles.push(file);
|
||||||
|
|
@ -116,12 +95,9 @@ export function InputArea({
|
||||||
const processFiles = (files: File[]) => {
|
const processFiles = (files: File[]) => {
|
||||||
const validFiles: File[] = [];
|
const validFiles: File[] = [];
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
const result = validateFile(file);
|
||||||
console.warn(`不支持的文件类型: ${file.type}`);
|
if (!result.valid) {
|
||||||
return;
|
alert(result.error);
|
||||||
}
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
console.warn(`文件过大: ${file.name}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
validFiles.push(file);
|
validFiles.push(file);
|
||||||
|
|
@ -301,7 +277,7 @@ export function InputArea({
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
accept={ALLOWED_TYPES.join(',')}
|
accept={[...ALLOWED_FILE_TYPES.image, ...ALLOWED_FILE_TYPES.document].join(',')}
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,18 @@ export const ALLOWED_FILE_TYPES = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
||||||
export const MAX_DIRECT_UPLOAD_SIZE = 10 * 1024 * 1024; // 10MB
|
export const MAX_DIRECT_UPLOAD_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
|
/** Claude API 文件大小限制(按类型区分) */
|
||||||
|
export const FILE_SIZE_LIMITS = {
|
||||||
|
/** 图片: Claude API 硬限制 5MB */
|
||||||
|
image: 5 * 1024 * 1024,
|
||||||
|
/** PDF: Claude API 整个请求 32MB 限制,单文件留 25MB */
|
||||||
|
pdf: 25 * 1024 * 1024,
|
||||||
|
/** 其他文档(Word/Excel/Text 等): 通用 50MB 上传限制 */
|
||||||
|
document: 50 * 1024 * 1024,
|
||||||
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证文件类型和大小
|
* 验证文件类型和大小
|
||||||
*/
|
*/
|
||||||
|
|
@ -61,10 +70,26 @@ export function validateFile(file: File): { valid: boolean; error?: string } {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
// 按文件类型检查大小限制
|
||||||
|
const fileType = getFileType(file.type);
|
||||||
|
let maxSize: number;
|
||||||
|
let typeLabel: string;
|
||||||
|
|
||||||
|
if (fileType === 'image') {
|
||||||
|
maxSize = FILE_SIZE_LIMITS.image;
|
||||||
|
typeLabel = '图片';
|
||||||
|
} else if (file.type === 'application/pdf') {
|
||||||
|
maxSize = FILE_SIZE_LIMITS.pdf;
|
||||||
|
typeLabel = 'PDF';
|
||||||
|
} else {
|
||||||
|
maxSize = FILE_SIZE_LIMITS.document;
|
||||||
|
typeLabel = '文档';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `文件大小超过限制。最大允许: ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
error: `${typeLabel}大小超过限制 (${(file.size / 1024 / 1024).toFixed(1)}MB)。${typeLabel}最大允许: ${maxSize / 1024 / 1024}MB`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue