备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
@@ -1,185 +1,203 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Upload, Modal, Image, Spin, Progress, message } from 'antd';
|
||||
import { PlusOutlined, FileOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile, UploadProps } from 'antd/es/upload/interface';
|
||||
|
||||
interface FileUploadProps {
|
||||
value?: string[];
|
||||
onChange?: (urls: string[]) => void;
|
||||
maxCount?: number;
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
// 支持的图片格式
|
||||
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
|
||||
const officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
|
||||
const isImage = (url: string) => {
|
||||
const ext = url.split('.').pop()?.toLowerCase();
|
||||
return imageFormats.includes(ext || '');
|
||||
};
|
||||
|
||||
const isOfficeFile = (url: string) => {
|
||||
const ext = url.split('.').pop()?.toLowerCase();
|
||||
return officeFormats.includes(ext || '');
|
||||
};
|
||||
|
||||
const getOfficePreviewUrl = (url: string) => {
|
||||
// 使用微软的Office 365在线预览服务
|
||||
return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`;
|
||||
};
|
||||
|
||||
const FileUpload: React.FC<FileUploadProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
maxCount = 9,
|
||||
accept = 'image/*'
|
||||
}) => {
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// 当 value 变化时,更新 fileList
|
||||
useEffect(() => {
|
||||
// 只有当 value 是数组时才更新 fileList
|
||||
// 这样可以避免在上传过程中被重置
|
||||
if (Array.isArray(value)) {
|
||||
const newFileList = value.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: url.split('/').pop() || `file-${index}`,
|
||||
status: 'done',
|
||||
url,
|
||||
thumbUrl: isImage(url) ? url : undefined
|
||||
}));
|
||||
|
||||
setFileList(newFileList);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
const url = file.url || '';
|
||||
if (isImage(url)) {
|
||||
setPreviewImage(url);
|
||||
setPreviewOpen(true);
|
||||
} else if (isOfficeFile(url)) {
|
||||
// Office文件,使用微软的在线预览服务
|
||||
const previewUrl = getOfficePreviewUrl(url);
|
||||
window.open(previewUrl, '_blank');
|
||||
} else {
|
||||
// 其他文件,新窗口打开
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (info) => {
|
||||
const { fileList } = info;
|
||||
setFileList(fileList);
|
||||
|
||||
// 只有当文件状态发生变化时才调用 onChange
|
||||
// 避免在初始化时触发无限循环
|
||||
if (info.file.status === 'done' || info.file.status === 'removed') {
|
||||
// 提取已上传成功的URL
|
||||
const urls = fileList
|
||||
.filter(file => file.status === 'done')
|
||||
.map(file => {
|
||||
// 处理不同格式的文件对象
|
||||
if (file.url) {
|
||||
return file.url;
|
||||
} else if (file.response && file.response.url) {
|
||||
return file.response.url;
|
||||
} else if (file.response && typeof file.response === 'string') {
|
||||
return file.response;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(url => url); // 过滤空字符串
|
||||
|
||||
onChange?.(urls);
|
||||
}
|
||||
};
|
||||
|
||||
const customRequest = async (options: any) => {
|
||||
const { file, onSuccess, onError, onProgress } = options;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload/single', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
onProgress({ percent: 100 });
|
||||
// 传递包含url属性的对象,这是Ant Design Upload组件在customRequest中期望的格式
|
||||
onSuccess({ url: data.data.url }, file);
|
||||
message.success('上传成功');
|
||||
} else {
|
||||
onError(new Error(data.error));
|
||||
message.error(data.error || '上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传错误:', error);
|
||||
onError(error);
|
||||
message.error('上传失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>上传</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
customRequest={customRequest}
|
||||
accept={accept}
|
||||
maxCount={maxCount}
|
||||
multiple
|
||||
>
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
|
||||
{/* 图片预览弹窗 */}
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
title="图片预览"
|
||||
footer={null}
|
||||
onCancel={() => setPreviewOpen(false)}
|
||||
width="80%"
|
||||
centered
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
style={{ maxWidth: '100%', maxHeight: '80vh' }}
|
||||
preview={false}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{uploading && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Spin size="small" /> 上传中...
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Upload, Modal, Image, Spin, Progress, message } from 'antd';
|
||||
import { PlusOutlined, FileOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile, UploadProps } from 'antd/es/upload/interface';
|
||||
import { useLanguageStore } from '../store/languageStore';
|
||||
|
||||
interface FileUploadProps {
|
||||
value?: string[];
|
||||
onChange?: (urls: string[]) => void;
|
||||
maxCount?: number;
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
// 支持的图片格式
|
||||
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
|
||||
const officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
|
||||
const isImage = (url: string) => {
|
||||
const ext = url.split('.').pop()?.toLowerCase();
|
||||
return imageFormats.includes(ext || '');
|
||||
};
|
||||
|
||||
const isOfficeFile = (url: string) => {
|
||||
const ext = url.split('.').pop()?.toLowerCase();
|
||||
return officeFormats.includes(ext || '');
|
||||
};
|
||||
|
||||
const getOfficePreviewUrl = (url: string) => {
|
||||
// 使用微软的Office 365在线预览服务
|
||||
return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`;
|
||||
};
|
||||
|
||||
const FileUpload: React.FC<FileUploadProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
maxCount = 9,
|
||||
accept = 'image/*'
|
||||
}) => {
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const { t, currentLanguage } = useLanguageStore();
|
||||
|
||||
// 当 value 变化时,更新 fileList
|
||||
useEffect(() => {
|
||||
// 只有当 value 是数组时才更新 fileList
|
||||
// 这样可以避免在上传过程中被重置
|
||||
if (Array.isArray(value)) {
|
||||
const newFileList = value.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: url.split('/').pop() || `file-${index}`,
|
||||
status: 'done',
|
||||
url,
|
||||
thumbUrl: isImage(url) ? url : undefined
|
||||
}));
|
||||
|
||||
setFileList(newFileList);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
const url = file.url || '';
|
||||
if (isImage(url)) {
|
||||
setPreviewImage(url);
|
||||
setPreviewOpen(true);
|
||||
} else if (isOfficeFile(url)) {
|
||||
// Office文件,使用微软的在线预览服务
|
||||
const previewUrl = getOfficePreviewUrl(url);
|
||||
window.open(previewUrl, '_blank');
|
||||
} else {
|
||||
// 其他文件,新窗口打开
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (info) => {
|
||||
const { fileList } = info;
|
||||
setFileList(fileList);
|
||||
|
||||
// 只有当文件状态发生变化时才调用 onChange
|
||||
// 避免在初始化时触发无限循环
|
||||
if (info.file.status === 'done' || info.file.status === 'removed') {
|
||||
// 提取已上传成功的URL
|
||||
const urls = fileList
|
||||
.filter(file => file.status === 'done')
|
||||
.map(file => {
|
||||
// 处理不同格式的文件对象
|
||||
if (file.url) {
|
||||
return file.url;
|
||||
} else if (file.response && file.response.url) {
|
||||
return file.response.url;
|
||||
} else if (file.response && typeof file.response === 'string') {
|
||||
return file.response;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(url => url); // 过滤空字符串
|
||||
|
||||
onChange?.(urls);
|
||||
}
|
||||
};
|
||||
|
||||
const customRequest = async (options: any) => {
|
||||
const { file, onSuccess, onError, onProgress } = options;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// 从 localStorage 读取认证 token
|
||||
let authHeader = '';
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
const token = parsed?.state?.token;
|
||||
if (token) {
|
||||
authHeader = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
const res = await fetch('/api/upload/single', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: authHeader ? { Authorization: authHeader } : {},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
onProgress({ percent: 100 });
|
||||
// 传递包含url属性的对象,这是Ant Design Upload组件在customRequest中期望的格式
|
||||
onSuccess({ url: data.data.url }, file);
|
||||
message.success(t('fileUpload.uploadSuccess'));
|
||||
} else {
|
||||
onError(new Error(data.error));
|
||||
message.error(data.error || t('fileUpload.uploadFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传错误:', error);
|
||||
onError(error);
|
||||
message.error(t('fileUpload.uploadFailed'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>{t('fileUpload.upload')}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
customRequest={customRequest}
|
||||
accept={accept}
|
||||
maxCount={maxCount}
|
||||
multiple
|
||||
>
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
|
||||
{/* 图片预览弹窗 */}
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
title={t('fileUpload.preview')}
|
||||
footer={null}
|
||||
onCancel={() => setPreviewOpen(false)}
|
||||
width="80%"
|
||||
centered
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
style={{ maxWidth: '100%', maxHeight: '80vh' }}
|
||||
preview={false}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{uploading && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Spin size="small" /> {t('fileUpload.uploading')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
|
||||
Reference in New Issue
Block a user