mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat(upload): remove image format conversion options #6
- Remove image format conversion and quality settings from the upload dialog - Update backend to use default format and quality settings - Add system configuration options for default image format and quality
This commit is contained in:
@@ -74,7 +74,6 @@ public class PictureController(IPictureService pictureService, IConfigService co
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = GetCurrentUserId();
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
await using var stream = request.File.OpenReadStream();
|
await using var stream = request.File.OpenReadStream();
|
||||||
var result = await pictureService.UploadPictureAsync(
|
var result = await pictureService.UploadPictureAsync(
|
||||||
request.File.FileName,
|
request.File.FileName,
|
||||||
@@ -83,9 +82,7 @@ public class PictureController(IPictureService pictureService, IConfigService co
|
|||||||
userId,
|
userId,
|
||||||
(PermissionType)request.Permission!,
|
(PermissionType)request.Permission!,
|
||||||
request.AlbumId,
|
request.AlbumId,
|
||||||
request.StorageType,
|
request.StorageType
|
||||||
request.ConvertToFormat,
|
|
||||||
request.Quality
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var picture = result.Picture;
|
var picture = result.Picture;
|
||||||
|
|||||||
@@ -16,15 +16,4 @@ public record UploadPictureRequest
|
|||||||
public int? AlbumId { get; set; }
|
public int? AlbumId { get; set; }
|
||||||
|
|
||||||
public StorageType? StorageType { get; set; }
|
public StorageType? StorageType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 目标图片格式,默认为保持原格式
|
|
||||||
/// </summary>
|
|
||||||
public ImageFormat ConvertToFormat { get; set; } = ImageFormat.Original;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图片质量(仅对JPEG和WebP有效,1-100)
|
|
||||||
/// </summary>
|
|
||||||
[Range(1, 100, ErrorMessage = "图片质量必须在1-100之间")]
|
|
||||||
public int Quality { get; set; } = 95;
|
|
||||||
}
|
}
|
||||||
@@ -34,9 +34,7 @@ public interface IPictureService
|
|||||||
int? userId,
|
int? userId,
|
||||||
PermissionType permission = PermissionType.Public,
|
PermissionType permission = PermissionType.Public,
|
||||||
int? albumId = null,
|
int? albumId = null,
|
||||||
StorageType? storageType = null,
|
StorageType? storageType = null
|
||||||
ImageFormat convertToFormat = ImageFormat.Original,
|
|
||||||
int quality = 95
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Task<ExifInfo> GetPictureExifInfoAsync(int pictureId);
|
Task<ExifInfo> GetPictureExifInfoAsync(int pictureId);
|
||||||
|
|||||||
@@ -437,19 +437,17 @@ public class PictureService(
|
|||||||
int? userId,
|
int? userId,
|
||||||
PermissionType permission = PermissionType.Public,
|
PermissionType permission = PermissionType.Public,
|
||||||
int? albumId = null,
|
int? albumId = null,
|
||||||
StorageType? storageType = null,
|
StorageType? storageType = null)
|
||||||
ImageFormat convertToFormat = ImageFormat.Original,
|
|
||||||
int quality = 95)
|
|
||||||
{
|
{
|
||||||
StorageType GetConfigStorageType(string configKey)
|
StorageType GetConfigStorageType(string configKey)
|
||||||
{
|
{
|
||||||
string? configValue = configuration[configKey];
|
string? configValue = configuration[configKey];
|
||||||
return !string.IsNullOrEmpty(configValue) &&
|
return !string.IsNullOrEmpty(configValue) &&
|
||||||
Enum.TryParse<StorageType>(configValue, out var configStorageType)
|
Enum.TryParse<StorageType>(configValue, out var configStorageType)
|
||||||
? configStorageType
|
? configStorageType
|
||||||
: StorageType.Local;
|
: StorageType.Local;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == null)
|
if (userId == null)
|
||||||
{
|
{
|
||||||
storageType = GetConfigStorageType("Storage:AnonymousDefaultStorage");
|
storageType = GetConfigStorageType("Storage:AnonymousDefaultStorage");
|
||||||
@@ -458,7 +456,21 @@ public class PictureService(
|
|||||||
{
|
{
|
||||||
storageType = GetConfigStorageType("Storage:DefaultStorage");
|
storageType = GetConfigStorageType("Storage:DefaultStorage");
|
||||||
}
|
}
|
||||||
|
ImageFormat convertToFormat = ImageFormat.Original;
|
||||||
|
string defaultFormatConfig = configuration["Upload:DefaultImageFormat"];
|
||||||
|
if (!string.IsNullOrEmpty(defaultFormatConfig))
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<ImageFormat>(defaultFormatConfig, true, out var parsedFormat))
|
||||||
|
{
|
||||||
|
convertToFormat = parsedFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int quality = 100;
|
||||||
|
string defaultQualityConfig = configuration["Upload:DefaultImageQuality"];
|
||||||
|
if (!string.IsNullOrEmpty(defaultQualityConfig))
|
||||||
|
{
|
||||||
|
quality = int.Parse(defaultQualityConfig);
|
||||||
|
}
|
||||||
string originalFileName = fileName;
|
string originalFileName = fileName;
|
||||||
string finalFileName = fileName;
|
string finalFileName = fileName;
|
||||||
string finalContentType = contentType;
|
string finalContentType = contentType;
|
||||||
@@ -553,7 +565,7 @@ public class PictureService(
|
|||||||
AlbumId = albumId,
|
AlbumId = albumId,
|
||||||
StorageType = storageType.Value,
|
StorageType = storageType.Value,
|
||||||
ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending,
|
ProcessingStatus = isAnonymous ? ProcessingStatus.Completed : ProcessingStatus.Pending,
|
||||||
ThumbnailPath = isAnonymous ? relativePath : null
|
ThumbnailPath = isAnonymous ? relativePath : null
|
||||||
};
|
};
|
||||||
|
|
||||||
dbContext.Pictures.Add(picture);
|
dbContext.Pictures.Add(picture);
|
||||||
@@ -570,7 +582,7 @@ public class PictureService(
|
|||||||
Id = picture.Id,
|
Id = picture.Id,
|
||||||
Name = picture.Name,
|
Name = picture.Name,
|
||||||
Path = storageService.GetUrl(picture.StorageType, relativePath),
|
Path = storageService.GetUrl(picture.StorageType, relativePath),
|
||||||
ThumbnailPath = isAnonymous ? storageService.GetUrl(picture.StorageType, relativePath) : null,
|
ThumbnailPath = isAnonymous ? storageService.GetUrl(picture.StorageType, relativePath) : null,
|
||||||
Description = picture.Description,
|
Description = picture.Description,
|
||||||
CreatedAt = picture.CreatedAt,
|
CreatedAt = picture.CreatedAt,
|
||||||
Tags = new List<string>(),
|
Tags = new List<string>(),
|
||||||
@@ -590,7 +602,7 @@ public class PictureService(
|
|||||||
string tempFilePath = tempFileStream.Name;
|
string tempFilePath = tempFileStream.Name;
|
||||||
finalStream.Dispose();
|
finalStream.Dispose();
|
||||||
if (File.Exists(tempFilePath)) File.Delete(tempFilePath);
|
if (File.Exists(tempFilePath)) File.Delete(tempFilePath);
|
||||||
|
|
||||||
// 同时清理原始临时文件
|
// 同时清理原始临时文件
|
||||||
string tempOriginalFile = Path.ChangeExtension(tempFilePath, null);
|
string tempOriginalFile = Path.ChangeExtension(tempFilePath, null);
|
||||||
if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile);
|
if (File.Exists(tempOriginalFile)) File.Delete(tempOriginalFile);
|
||||||
|
|||||||
@@ -119,8 +119,6 @@ export async function uploadPicture(
|
|||||||
data: {
|
data: {
|
||||||
permission?: number;
|
permission?: number;
|
||||||
albumId?: number;
|
albumId?: number;
|
||||||
convertToFormat?: string;
|
|
||||||
quality?: number;
|
|
||||||
onProgress?: (percent: number) => void
|
onProgress?: (percent: number) => void
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<BaseResult<PictureResponse>> {
|
): Promise<BaseResult<PictureResponse>> {
|
||||||
@@ -135,14 +133,6 @@ export async function uploadPicture(
|
|||||||
formData.append('albumId', data.albumId.toString());
|
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 {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Upload, Button, Progress, message, Form, Select, Radio, Slider, Divider, Alert } from 'antd';
|
import { Modal, Upload, Button, Progress, message, Form, Select, Radio, Slider } from 'antd';
|
||||||
import { InboxOutlined } from '@ant-design/icons';
|
import { InboxOutlined } from '@ant-design/icons';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import type { UploadFile, AlbumResponse } from '../../api';
|
import type { UploadFile, AlbumResponse } from '../../api';
|
||||||
import { uploadPicture, getAlbums, ImageFormat } from '../../api';
|
import { uploadPicture, getAlbums } from '../../api';
|
||||||
|
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -20,8 +20,6 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [albums, setAlbums] = useState<AlbumResponse[]>([]);
|
const [albums, setAlbums] = useState<AlbumResponse[]>([]);
|
||||||
const [concurrentUploads, setConcurrentUploads] = useState<number>(3);
|
const [concurrentUploads, setConcurrentUploads] = useState<number>(3);
|
||||||
const [convertFormat, setConvertFormat] = useState<ImageFormat>(ImageFormat.Original);
|
|
||||||
const [quality, setQuality] = useState<number>(95);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -77,11 +75,9 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
setUploading(true);
|
setUploading(true);
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
||||||
const params: { // 修改此处的类型定义
|
const params: {
|
||||||
permission?: number;
|
permission?: number;
|
||||||
albumId?: number;
|
albumId?: number;
|
||||||
convertToFormat?: string; // 允许 string 类型
|
|
||||||
quality?: number;
|
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (values.permission !== undefined) {
|
if (values.permission !== undefined) {
|
||||||
@@ -90,10 +86,6 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
if (values.albumId) {
|
if (values.albumId) {
|
||||||
params.albumId = values.albumId;
|
params.albumId = values.albumId;
|
||||||
}
|
}
|
||||||
if (convertFormat !== ImageFormat.Original) {
|
|
||||||
params.convertToFormat = convertFormat.toString(); // 现在类型匹配
|
|
||||||
params.quality = quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
@@ -110,7 +102,7 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 上传文件
|
// 上传文件
|
||||||
const result = await uploadPicture(item.file, { // 此处 params 类型现在匹配
|
const result = await uploadPicture(item.file, {
|
||||||
...params,
|
...params,
|
||||||
onProgress: (percent) => {
|
onProgress: (percent) => {
|
||||||
setUploadQueue((prev) =>
|
setUploadQueue((prev) =>
|
||||||
@@ -217,17 +209,6 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ 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) => {
|
const renderUploadItem = (item: UploadFile) => {
|
||||||
let statusIcon;
|
let statusIcon;
|
||||||
@@ -361,51 +342,6 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
<Radio value={2}>仅自己</Radio>
|
<Radio value={2}>仅自己</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider orientation="left">格式转换设置</Divider>
|
|
||||||
|
|
||||||
<Form.Item label="输出格式">
|
|
||||||
<Radio.Group
|
|
||||||
value={convertFormat}
|
|
||||||
onChange={(e) => setConvertFormat(e.target.value)}
|
|
||||||
optionType="button"
|
|
||||||
buttonStyle="solid"
|
|
||||||
style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}
|
|
||||||
>
|
|
||||||
<Radio.Button value={ImageFormat.Original}>保持原格式</Radio.Button>
|
|
||||||
<Radio.Button value={ImageFormat.Jpeg}>JPEG (.jpg)</Radio.Button>
|
|
||||||
<Radio.Button value={ImageFormat.Png}>PNG (.png)</Radio.Button>
|
|
||||||
<Radio.Button value={ImageFormat.WebP}>WebP (.webp)</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{convertFormat !== ImageFormat.Original && (
|
|
||||||
<>
|
|
||||||
{convertFormat === ImageFormat.Png ? (
|
|
||||||
<Alert
|
|
||||||
message="提示"
|
|
||||||
description={`${getFormatName(convertFormat)} 格式为无损压缩,不支持质量调节。`}
|
|
||||||
type="info"
|
|
||||||
showIcon
|
|
||||||
style={{ marginBottom: '16px' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Form.Item label={`图片质量 (${quality}%)`}>
|
|
||||||
<Slider
|
|
||||||
min={50}
|
|
||||||
max={100}
|
|
||||||
value={quality}
|
|
||||||
onChange={setQuality}
|
|
||||||
marks={{ 50: '50%', 75: '75%', 90: '90%', 95: '95%', 100: '100%' }}
|
|
||||||
tooltip={{ formatter: (value) => `${value}%` }}
|
|
||||||
/>
|
|
||||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
|
||||||
质量越高文件越大,建议使用 85-95% 的质量设置
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="concurrentUploads"
|
name="concurrentUploads"
|
||||||
@@ -434,14 +370,10 @@ const ImageUploadDialog: React.FC<UploadDialogProps> = ({ visible, onClose, onUp
|
|||||||
<p className="ant-upload-text">点击或拖拽图片到此区域上传</p>
|
<p className="ant-upload-text">点击或拖拽图片到此区域上传</p>
|
||||||
<p className="ant-upload-hint">
|
<p className="ant-upload-hint">
|
||||||
支持单个或批量上传,图片大小不超过10MB
|
支持单个或批量上传,图片大小不超过10MB
|
||||||
{convertFormat !== ImageFormat.Original && (
|
<br />
|
||||||
<>
|
<span style={{ color: '#888' }}>
|
||||||
<br />
|
图片将按系统默认设置进行处理
|
||||||
<span style={{ color: '#1890ff' }}>
|
</span>
|
||||||
将转换为 {getFormatName(convertFormat)} 格式
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
|
|
||||||
|
|||||||
@@ -391,6 +391,90 @@ const SystemConfig: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 上传设置卡片 - 新增 */}
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title="上传设置配置"
|
||||||
|
style={{ marginBottom: isMobile ? 16 : 24 }}
|
||||||
|
bodyStyle={{ padding: isMobile ? '12px' : '16px' }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||||
|
gap: isMobile ? 12 : 16,
|
||||||
|
marginBottom: 0
|
||||||
|
}}>
|
||||||
|
{/* 图片默认格式 */}
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
默认图片格式
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={configs.Upload?.DefaultImageFormat || 'Original'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleSaveConfig('Upload', 'DefaultImageFormat', value);
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
size="large"
|
||||||
|
placeholder="选择上传图片的默认处理格式"
|
||||||
|
>
|
||||||
|
<Option value="Original">保持原始格式</Option>
|
||||||
|
<Option value="Jpeg">转换为JPEG</Option>
|
||||||
|
<Option value="Png">转换为PNG</Option>
|
||||||
|
<Option value="Webp">转换为WebP</Option>
|
||||||
|
</Select>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#999',
|
||||||
|
marginTop: 4
|
||||||
|
}}>
|
||||||
|
上传图片时的默认处理格式,选择合适的格式可以优化存储和显示
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片压缩质量 */}
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
默认压缩质量
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={configs.Upload?.DefaultImageQuality || '95'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleSaveConfig('Upload', 'DefaultImageQuality', value);
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
size="large"
|
||||||
|
placeholder="选择图片压缩质量"
|
||||||
|
>
|
||||||
|
<Option value="100">100% - 最高质量</Option>
|
||||||
|
<Option value="95">95% - 高质量</Option>
|
||||||
|
<Option value="90">90% - 优质</Option>
|
||||||
|
<Option value="85">85% - 良好</Option>
|
||||||
|
<Option value="80">80% - 节省空间</Option>
|
||||||
|
<Option value="75">75% - 平衡</Option>
|
||||||
|
<Option value="70">70% - 压缩</Option>
|
||||||
|
</Select>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#999',
|
||||||
|
marginTop: 4
|
||||||
|
}}>
|
||||||
|
适用于JPEG和WebP格式的图片质量设置,越高图片质量越好但文件越大
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 存储服务配置卡片 */}
|
{/* 存储服务配置卡片 */}
|
||||||
<Card
|
<Card
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
Reference in New Issue
Block a user