diff --git a/Api/PictureController.cs b/Api/PictureController.cs index 1b401d0..66a46e7 100644 --- a/Api/PictureController.cs +++ b/Api/PictureController.cs @@ -83,7 +83,9 @@ public class PictureController(IPictureService pictureService, IConfigService co userId, (PermissionType)request.Permission!, request.AlbumId, - request.StorageType + request.StorageType, + request.ConvertToFormat, + request.Quality ); var picture = result.Picture; diff --git a/Models/Request/Picture/UploadPictureRequest.cs b/Models/Request/Picture/UploadPictureRequest.cs index 6e1553b..908edfd 100644 --- a/Models/Request/Picture/UploadPictureRequest.cs +++ b/Models/Request/Picture/UploadPictureRequest.cs @@ -1,16 +1,30 @@ using System.ComponentModel.DataAnnotations; using Foxel.Models.DataBase; +using Foxel.Models.Enums; using Foxel.Services.Attributes; namespace Foxel.Models.Request.Picture; public record UploadPictureRequest { - [Required] public IFormFile File { get; set; } = null!; + [Required(ErrorMessage = "文件不能为空")] + public IFormFile File { get; set; } = null!; - public int? Permission { get; set; } = 1; + [Range(0, 2, ErrorMessage = "权限类型必须是0(公开)、1(私有)或2(仅关注者)")] + public int? Permission { get; set; } = 0; - public int? AlbumId { get; set; } = null; - - public StorageType? StorageType { get; set; } = null; + public int? AlbumId { get; set; } + + public StorageType? StorageType { get; set; } + + /// + /// 目标图片格式,默认为保持原格式 + /// + public ImageFormat ConvertToFormat { get; set; } = ImageFormat.Original; + + /// + /// 图片质量(仅对JPEG和WebP有效,1-100) + /// + [Range(1, 100, ErrorMessage = "图片质量必须在1-100之间")] + public int Quality { get; set; } = 95; } \ No newline at end of file diff --git a/Services/Background/BackgroundTaskQueue.cs b/Services/Background/BackgroundTaskQueue.cs index af06fe7..bc7b566 100644 --- a/Services/Background/BackgroundTaskQueue.cs +++ b/Services/Background/BackgroundTaskQueue.cs @@ -238,8 +238,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable await UpdatePictureStatus(task.PictureId, ProcessingStatus.Processing, 20); var thumbnailPath = Path.Combine( Path.GetDirectoryName(localFilePath)!, - Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb" + - Path.GetExtension(localFilePath)); + Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb.webp"); await ImageHelper.CreateThumbnailAsync(localFilePath, thumbnailPath, 500); @@ -257,7 +256,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable // 非本地存储,上传缩略图到对应的存储服务 await using var thumbnailFileStream = new FileStream(thumbnailPath, FileMode.Open, FileAccess.Read); var thumbnailFileName = Path.GetFileName(thumbnailPath); - var thumbnailContentType = Path.GetExtension(thumbnailPath).ToLower() == ".png" ? "image/png" : "image/jpeg"; + var thumbnailContentType = "image/webp"; // 上传缩略图并获取存储路径或元数据 string thumbnailStoragePath = await storageService.SaveAsync( diff --git a/Services/Media/IPictureService.cs b/Services/Media/IPictureService.cs index b1c6902..6ed9472 100644 --- a/Services/Media/IPictureService.cs +++ b/Services/Media/IPictureService.cs @@ -1,5 +1,6 @@ using Foxel.Models; using Foxel.Models.DataBase; +using Foxel.Models.Enums; using Foxel.Models.Response.Picture; using Foxel.Services.Attributes; @@ -33,7 +34,9 @@ public interface IPictureService int? userId, PermissionType permission = PermissionType.Public, int? albumId = null, - StorageType? storageType = null + StorageType? storageType = null, + ImageFormat convertToFormat = ImageFormat.Original, + int quality = 95 ); Task GetPictureExifInfoAsync(int pictureId); diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index 1740708..8cf8e53 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Foxel.Models; using Foxel.Models.DataBase; +using Foxel.Models.Enums; using Foxel.Models.Response.Picture; using Foxel.Services.AI; using Foxel.Services.Attributes; @@ -436,7 +437,9 @@ public class PictureService( int? userId, PermissionType permission = PermissionType.Public, int? albumId = null, - StorageType? storageType = null) + StorageType? storageType = null, + ImageFormat convertToFormat = ImageFormat.Original, + int quality = 95) { // 如果未指定存储类型,则从配置中获取默认存储类型 if (storageType == null) @@ -449,88 +452,143 @@ public class PictureService( storageType = defaultStorageType; } - string fileExtension = Path.GetExtension(fileName); - _ = $"{Guid.NewGuid()}{fileExtension}"; + string originalFileName = fileName; + string finalFileName = fileName; + string finalContentType = contentType; + Stream finalStream = fileStream; - // 使用存储服务保存文件 - string relativePath = await storageService.SaveAsync(storageType.Value, fileStream, fileName, contentType); - - // 创建基本的Picture对象,使用文件名作为标题和描述 - string initialTitle = Path.GetFileNameWithoutExtension(fileName); - string initialDescription = $"Uploaded on {DateTime.UtcNow}"; - - await using var dbContext = await contextFactory.CreateDbContextAsync(); - - // 获取用户 - User? user = null; - if (userId is not null) + // 如果需要格式转换 + if (convertToFormat != ImageFormat.Original) { - user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); - if (user == null) + // 创建临时文件保存原始上传内容 + string tempOriginalFile = Path.GetTempFileName(); + string tempConvertedFile = Path.GetTempFileName(); + + try { - throw new Exception("找不到指定的用户"); + // 保存原始文件到临时位置 + await using (var tempFileStream = new FileStream(tempOriginalFile, FileMode.Create)) + { + await fileStream.CopyToAsync(tempFileStream); + } + + // 转换格式 + string convertedFilePath = await ImageHelper.ConvertImageFormatAsync( + tempOriginalFile, tempConvertedFile, convertToFormat, quality); + + // 更新文件信息 + string newExtension = ImageHelper.GetFileExtensionFromFormat(convertToFormat); + finalFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalFileName), newExtension); + finalContentType = ImageHelper.GetMimeTypeFromFormat(convertToFormat); + + // 创建新的流用于上传转换后的文件 + finalStream = new FileStream(convertedFilePath, FileMode.Open, FileAccess.Read); + } + catch + { + // 清理临时文件 + if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile); + if (File.Exists(tempConvertedFile)) File.Delete(tempConvertedFile); + throw; } } - // 检查相册是否存在并且属于当前用户 - Album? album = null; - if (albumId.HasValue) + try { - album = await dbContext.Albums.Include(a => a.User) - .FirstOrDefaultAsync(a => a.Id == albumId.Value); + // 使用存储服务保存文件 + string relativePath = await storageService.SaveAsync(storageType.Value, finalStream, finalFileName, finalContentType); - if (album == null) + // 创建基本的Picture对象,使用文件名作为标题和描述 + string initialTitle = Path.GetFileNameWithoutExtension(originalFileName); + string initialDescription = $"Uploaded on {DateTime.UtcNow}"; + + await using var dbContext = await contextFactory.CreateDbContextAsync(); + + // 获取用户 + User? user = null; + if (userId is not null) { - throw new KeyNotFoundException($"找不到ID为{albumId.Value}的相册"); + user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); + if (user == null) + { + throw new Exception("找不到指定的用户"); + } } - if (album.User.Id != userId) + // 检查相册是否存在并且属于当前用户 + Album? album = null; + if (albumId.HasValue) { - throw new Exception("您无权将图片添加到此相册"); + album = await dbContext.Albums.Include(a => a.User) + .FirstOrDefaultAsync(a => a.Id == albumId.Value); + + if (album == null) + { + throw new KeyNotFoundException($"找不到ID为{albumId.Value}的相册"); + } + + if (album.User.Id != userId) + { + throw new Exception("您无权将图片添加到此相册"); + } + } + + bool isAnonymous = userId == null; + + // 创建图片对象并保存到数据库 + var picture = new Picture + { + Name = initialTitle, + Description = initialDescription, + Path = relativePath, + User = user, + Permission = permission, + AlbumId = albumId, + StorageType = storageType.Value, + ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending, + ThumbnailPath = isAnonymous ? relativePath : null + }; + + dbContext.Pictures.Add(picture); + await dbContext.SaveChangesAsync(); + + if (!isAnonymous) + { + await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, relativePath); + } + + // 返回图片基本信息 + var pictureResponse = new PictureResponse + { + Id = picture.Id, + Name = picture.Name, + Path = storageService.GetUrl(picture.StorageType, relativePath), + ThumbnailPath = isAnonymous ? storageService.GetUrl(picture.StorageType, relativePath) : null, + Description = picture.Description, + CreatedAt = picture.CreatedAt, + Tags = new List(), + Permission = permission, + AlbumId = albumId, + AlbumName = album?.Name, + ProcessingStatus = picture.ProcessingStatus + }; + + return (pictureResponse, picture.Id); + } + finally + { + // 清理转换后的临时流 + if (finalStream != fileStream && finalStream is FileStream tempFileStream) + { + string tempFilePath = tempFileStream.Name; + finalStream.Dispose(); + if (File.Exists(tempFilePath)) File.Delete(tempFilePath); + + // 同时清理原始临时文件 + string tempOriginalFile = Path.ChangeExtension(tempFilePath, null); + if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile); } } - - bool isAnonymous = userId == null; - - // 创建图片对象并保存到数据库 - var picture = new Picture - { - Name = initialTitle, - Description = initialDescription, - Path = relativePath, - User = user, - Permission = permission, - AlbumId = albumId, - StorageType = storageType.Value, - ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending, - ThumbnailPath = isAnonymous ? relativePath : null - }; - - dbContext.Pictures.Add(picture); - await dbContext.SaveChangesAsync(); - - if (!isAnonymous) - { - await backgroundTaskQueue.QueuePictureProcessingTaskAsync(picture.Id, relativePath); - } - - // 返回图片基本信息 - var pictureResponse = new PictureResponse - { - Id = picture.Id, - Name = picture.Name, - Path = storageService.GetUrl(picture.StorageType, relativePath), - ThumbnailPath = isAnonymous ? storageService.GetUrl(picture.StorageType, relativePath) : null, - Description = picture.Description, - CreatedAt = picture.CreatedAt, - Tags = new List(), - Permission = permission, - AlbumId = albumId, - AlbumName = album?.Name, - ProcessingStatus = picture.ProcessingStatus - }; - - return (pictureResponse, picture.Id); } public async Task GetPictureExifInfoAsync(int pictureId) diff --git a/Utils/ImageHelper.cs b/Utils/ImageHelper.cs index dcb35f3..3955fe8 100644 --- a/Utils/ImageHelper.cs +++ b/Utils/ImageHelper.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using System.Globalization; using Foxel.Models; +using Foxel.Models.Enums; using SixLabors.ImageSharp.PixelFormats; namespace Foxel.Utils; @@ -53,54 +54,26 @@ public static class ImageHelper Mode = ResizeMode.Max })); - string extension = Path.GetExtension(thumbnailPath).ToLower(); + // 强制使用 WebP 格式,修改缩略图路径扩展名 + string webpThumbnailPath = Path.ChangeExtension(thumbnailPath, ".webp"); - // 根据原图大小动态调整质量 - int adjustedQuality = AdjustQualityByFileSize(originalSize, extension, quality); + int adjustedQuality = AdjustQualityByFileSize(originalSize, ".webp", quality); - if (extension == ".jpg" || extension == ".jpeg") + await image.SaveAsWebpAsync(webpThumbnailPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder { - await image.SaveAsJpegAsync(thumbnailPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder - { - Quality = adjustedQuality - }); - } - else if (extension == ".png") - { - await image.SaveAsPngAsync(thumbnailPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder - { - CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression, - ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha, // 确保使用最优的颜色类型 - FilterMethod = SixLabors.ImageSharp.Formats.Png.PngFilterMethod.Adaptive // 使用自适应过滤 - }); - } - else - { - await image.SaveAsync(thumbnailPath); - } + Quality = adjustedQuality, + Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality + }); - var thumbnailFileInfo = new FileInfo(thumbnailPath); + var thumbnailFileInfo = new FileInfo(webpThumbnailPath); if (thumbnailFileInfo.Length < originalSize) return thumbnailFileInfo.Length; - // 再次尝试优化,但不改变扩展名 - if (extension == ".png") + await image.SaveAsWebpAsync(webpThumbnailPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder { - await image.SaveAsPngAsync(thumbnailPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder - { - CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression, - FilterMethod = SixLabors.ImageSharp.Formats.Png.PngFilterMethod.Adaptive - }); - thumbnailFileInfo = new FileInfo(thumbnailPath); - } - else if (extension == ".jpg" || extension == ".jpeg") - { - // 如果是 JPEG,尝试降低质量进一步压缩 - await image.SaveAsJpegAsync(thumbnailPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder - { - Quality = Math.Max(adjustedQuality - 10, 60) // 再降低质量但不低于60 - }); - thumbnailFileInfo = new FileInfo(thumbnailPath); - } + Quality = Math.Max(adjustedQuality - 15, 50), + Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality + }); + thumbnailFileInfo = new FileInfo(webpThumbnailPath); return thumbnailFileInfo.Length; } @@ -162,7 +135,16 @@ public static class ImageHelper /// private static int AdjustQualityByFileSize(long originalSize, string extension, int baseQuality) { - if (extension == ".jpg" || extension == ".jpeg") + if (extension == ".webp") + { + if (originalSize > 10 * 1024 * 1024) // 10MB + return Math.Min(baseQuality, 70); + else if (originalSize > 5 * 1024 * 1024) // 5MB + return Math.Min(baseQuality, 75); + else if (originalSize > 1 * 1024 * 1024) // 1MB + return Math.Min(baseQuality, 80); + } + else if (extension == ".jpg" || extension == ".jpeg") { if (originalSize > 10 * 1024 * 1024) // 10MB return Math.Min(baseQuality, 65); @@ -342,4 +324,125 @@ public static class ImageHelper return null; } + + /// + /// 转换图片格式(无损转换并保留EXIF信息) + /// + /// 输入图片路径 + /// 输出图片路径 + /// 目标格式 + /// 压缩质量(仅对JPEG和WebP有效,1-100) + /// 转换后的文件路径 + public static async Task ConvertImageFormatAsync(string inputPath, string outputPath, ImageFormat targetFormat, int quality = 95) + { + if (targetFormat == ImageFormat.Original) + { + // 如果是原格式,直接返回输入路径 + return inputPath; + } + + using var image = await Image.LoadAsync(inputPath); + + // 保留原始EXIF信息 + var originalExifProfile = image.Metadata.ExifProfile; + + // 根据目标格式确定文件扩展名和输出路径 + string extension = GetFileExtensionFromFormat(targetFormat); + string finalOutputPath = Path.ChangeExtension(outputPath, extension); + + switch (targetFormat) + { + case ImageFormat.Jpeg: + await image.SaveAsJpegAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder + { + Quality = quality + }); + break; + + case ImageFormat.Png: + await image.SaveAsPngAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder + { + CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression, + ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha + }); + break; + + case ImageFormat.WebP: + await image.SaveAsWebpAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder + { + Quality = quality, + Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality + }); + break; + default: + throw new NotSupportedException($"不支持的图片格式: {targetFormat}"); + } + + // 如果原图有EXIF信息,保存到转换后的图片中 + if (originalExifProfile != null) + { + using var convertedImage = await Image.LoadAsync(finalOutputPath); + convertedImage.Metadata.ExifProfile = originalExifProfile; + + switch (targetFormat) + { + case ImageFormat.Jpeg: + await convertedImage.SaveAsJpegAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder + { + Quality = quality + }); + break; + + case ImageFormat.Png: + await convertedImage.SaveAsPngAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder + { + CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression, + ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha + }); + break; + + case ImageFormat.WebP: + await convertedImage.SaveAsWebpAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder + { + Quality = quality, + Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality + }); + break; + } + } + + return finalOutputPath; + } + + /// + /// 根据图片格式获取文件扩展名 + /// + /// 图片格式 + /// 文件扩展名 + public static string GetFileExtensionFromFormat(ImageFormat format) + { + return format switch + { + ImageFormat.Jpeg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.WebP => ".webp", + _ => throw new NotSupportedException($"不支持的图片格式: {format}") + }; + } + + /// + /// 根据图片格式获取MIME类型 + /// + /// 图片格式 + /// MIME类型 + public static string GetMimeTypeFromFormat(ImageFormat format) + { + return format switch + { + ImageFormat.Jpeg => "image/jpeg", + ImageFormat.Png => "image/png", + ImageFormat.WebP => "image/webp", + _ => throw new NotSupportedException($"不支持的图片格式: {format}") + }; + } } \ No newline at end of file diff --git a/Web/src/api/pictureApi.ts b/Web/src/api/pictureApi.ts index 8ae62ed..3f4cb07 100644 --- a/Web/src/api/pictureApi.ts +++ b/Web/src/api/pictureApi.ts @@ -119,6 +119,8 @@ export async function uploadPicture( data: { permission?: number; albumId?: number; + convertToFormat?: string; + quality?: number; onProgress?: (percent: number) => void } = {} ): Promise> { @@ -133,6 +135,14 @@ export async function uploadPicture( formData.append('albumId', data.albumId.toString()); } + if (data.convertToFormat !== undefined) { + formData.append('convertToFormat', data.convertToFormat.toString()); + } + + if (data.quality !== undefined) { + formData.append('quality', data.quality.toString()); + } + try { const token = localStorage.getItem('token'); const headers: Record = {}; diff --git a/Web/src/api/types.ts b/Web/src/api/types.ts index e602ded..60faa07 100644 --- a/Web/src/api/types.ts +++ b/Web/src/api/types.ts @@ -103,10 +103,24 @@ export interface UploadFile { response?: PictureResponse; // 上传成功后的响应 } -// 上传图片请求参数 +// 图片格式类型 +export type ImageFormat = 0 | 1 | 2 | 3; + +// 添加常量对象提供运行时值 +export const ImageFormat = { + Original: 0 as ImageFormat, + Jpeg: 1 as ImageFormat, + Png: 2 as ImageFormat, + WebP: 3 as ImageFormat +}; + +// 上传图片参数 export interface UploadPictureParams { - permission?: number; // 权限设置,默认为0(公开) - albumId?: number; // 相册ID,可选 + permission?: number; + albumId?: number; + convertToFormat?: ImageFormat; + quality?: number; + onProgress?: (percent: number) => void; } // 相册响应数据 diff --git a/Web/src/components/upload/ImageUploadDialog.tsx b/Web/src/components/upload/ImageUploadDialog.tsx index db57b3a..774e4f7 100644 --- a/Web/src/components/upload/ImageUploadDialog.tsx +++ b/Web/src/components/upload/ImageUploadDialog.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Upload, Button, Progress, message, Form, Select, Radio, Slider } from 'antd'; +import { Modal, Upload, Button, Progress, message, Form, Select, Radio, Slider, Divider, Alert } from 'antd'; import { InboxOutlined } from '@ant-design/icons'; import { v4 as uuidv4 } from 'uuid'; -import type { UploadFile, UploadPictureParams, AlbumResponse } from '../../api'; -import { uploadPicture, getAlbums } from '../../api'; +import type { UploadFile, AlbumResponse } from '../../api'; +import { uploadPicture, getAlbums, ImageFormat } from '../../api'; const { Dragger } = Upload; const { Option } = Select; @@ -20,6 +20,8 @@ const ImageUploadDialog: React.FC = ({ visible, onClose, onUp const [form] = Form.useForm(); const [albums, setAlbums] = useState([]); const [concurrentUploads, setConcurrentUploads] = useState(3); + const [convertFormat, setConvertFormat] = useState(ImageFormat.Original); + const [quality, setQuality] = useState(95); useEffect(() => { if (visible) { @@ -75,13 +77,23 @@ const ImageUploadDialog: React.FC = ({ visible, onClose, onUp setUploading(true); const values = await form.validateFields(); - const params: UploadPictureParams = {}; + const params: { // 修改此处的类型定义 + permission?: number; + albumId?: number; + convertToFormat?: string; // 允许 string 类型 + quality?: number; + } = {}; + if (values.permission !== undefined) { params.permission = values.permission; } if (values.albumId) { params.albumId = values.albumId; } + if (convertFormat !== ImageFormat.Original) { + params.convertToFormat = convertFormat.toString(); // 现在类型匹配 + params.quality = quality; + } let successCount = 0; let failCount = 0; @@ -98,7 +110,7 @@ const ImageUploadDialog: React.FC = ({ visible, onClose, onUp try { // 上传文件 - const result = await uploadPicture(item.file, { + const result = await uploadPicture(item.file, { // 此处 params 类型现在匹配 ...params, onProgress: (percent) => { setUploadQueue((prev) => @@ -205,6 +217,17 @@ const ImageUploadDialog: React.FC = ({ visible, onClose, onUp } }; + // 获取格式名称 + const getFormatName = (format: ImageFormat) => { + switch (format) { + case ImageFormat.Original: return '保持原格式'; + case ImageFormat.Jpeg: return 'JPEG'; + case ImageFormat.Png: return 'PNG'; + case ImageFormat.WebP: return 'WebP'; + default: return '未知格式'; + } + }; + // 自定义上传列表项 const renderUploadItem = (item: UploadFile) => { let statusIcon; @@ -305,9 +328,9 @@ const ImageUploadDialog: React.FC = ({ visible, onClose, onUp onClick={uploadFiles} > {uploading ? '正在上传...' : '开始上传'} - , + ]} - width={600} + width={700} >
= ({ visible, onClose, onUp 公开 好友可见 仅自己 - + + 格式转换设置 + + + setConvertFormat(e.target.value)} + optionType="button" + buttonStyle="solid" + style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }} + > + 保持原格式 + JPEG (.jpg) + PNG (.png) + WebP (.webp) + + + + {convertFormat !== ImageFormat.Original && ( + <> + {convertFormat === ImageFormat.Png ? ( + + ) : ( + + `${value}%` }} + /> +
+ 质量越高文件越大,建议使用 85-95% 的质量设置 +
+
+ )} + + )} = ({ visible, onClose, onUp

点击或拖拽图片到此区域上传

支持单个或批量上传,图片大小不超过10MB + {convertFormat !== ImageFormat.Original && ( + <> +
+ + 将转换为 {getFormatName(convertFormat)} 格式 + + + )}