feat: 支持按目标大小压缩图片

- 新增 target_size_bytes 参数,支持直接指定压缩后的目标大小(字节)
- 实现自动缩放算法:当仅调整质量无法达到目标时,自动缩小图片尺寸
- 前端新增压缩模式切换:百分比模式 / 目标大小模式
- 支持 KB/MB 单位选择
- 优化二分搜索算法,提高目标大小的精准度

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 17:51:15 +08:00
parent df9c40e456
commit 5f432998a3
5 changed files with 288 additions and 27 deletions

View File

@@ -20,8 +20,13 @@ interface UploadItem {
const auth = useAuthStore()
type CompressionMode = 'percent' | 'size'
const options = reactive({
mode: 'percent' as CompressionMode, // 压缩模式:百分比 / 目标大小
compressionRate: 60,
targetSize: '' as string, // 目标大小数值
targetUnit: 'KB' as 'KB' | 'MB', // 目标大小单位
maxWidth: '' as string,
maxHeight: '' as string,
})
@@ -105,18 +110,33 @@ function toInt(v: string): number | undefined {
return Math.floor(n)
}
function getTargetSizeBytes(): number | undefined {
if (options.mode !== 'size') return undefined
const size = Number(options.targetSize)
if (!Number.isFinite(size) || size <= 0) return undefined
return options.targetUnit === 'MB' ? size * 1024 * 1024 : size * 1024
}
async function runOne(item: UploadItem) {
item.status = 'compressing'
item.error = undefined
try {
const compressOptions = options.mode === 'percent'
? {
compression_rate: options.compressionRate,
max_width: toInt(options.maxWidth),
max_height: toInt(options.maxHeight),
}
: {
target_size_bytes: getTargetSizeBytes(),
max_width: toInt(options.maxWidth),
max_height: toInt(options.maxHeight),
}
const result = await compressFile(
item.file,
{
compression_rate: options.compressionRate,
max_width: toInt(options.maxWidth),
max_height: toInt(options.maxHeight),
},
compressOptions,
auth.token,
)
@@ -460,7 +480,31 @@ async function resendVerification() {
<div class="text-sm font-medium text-slate-900">压缩参数</div>
<div class="mt-4 grid grid-cols-1 gap-4">
<label class="space-y-1">
<!-- 压缩模式切换 -->
<div class="space-y-2">
<div class="text-xs font-medium text-slate-600">压缩模式</div>
<div class="flex rounded-lg border border-slate-200 p-1">
<button
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
:class="options.mode === 'percent' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
@click="options.mode = 'percent'"
>
按百分比
</button>
<button
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
:class="options.mode === 'size' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
@click="options.mode = 'size'"
>
按目标大小
</button>
</div>
</div>
<!-- 百分比模式 -->
<label v-if="options.mode === 'percent'" class="space-y-1">
<div class="flex items-center justify-between text-xs font-medium text-slate-600">
<span>压缩率</span>
<span class="text-slate-500">{{ options.compressionRate }}%</span>
@@ -476,6 +520,28 @@ async function resendVerification() {
<div class="text-xs text-slate-500">数值越小压缩越强目标为压缩后体积占原图比例100% 为不压缩</div>
</label>
<!-- 目标大小模式 -->
<div v-else class="space-y-1">
<div class="text-xs font-medium text-slate-600">目标大小</div>
<div class="flex gap-2">
<input
v-model="options.targetSize"
type="number"
min="1"
placeholder="例如 50"
class="flex-1 rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
/>
<select
v-model="options.targetUnit"
class="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
>
<option value="KB">KB</option>
<option value="MB">MB</option>
</select>
</div>
<div class="text-xs text-slate-500">直接指定压缩后的目标大小系统会自动调整质量以逼近目标 JPEG/WebP/AVIF 支持</div>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">最大宽度px</div>

View File

@@ -84,6 +84,7 @@ export interface CompressResponse {
export interface CompressOptions {
level?: CompressionLevel
compression_rate?: number
target_size_bytes?: number // 新增:直接指定目标大小(字节)
output_format?: OutputFormat
max_width?: number
max_height?: number
@@ -96,6 +97,7 @@ export async function compressFile(file: File, options: CompressOptions, token?:
if (options.level) form.append('level', options.level)
if (options.compression_rate) form.append('compression_rate', String(options.compression_rate))
if (options.target_size_bytes) form.append('target_size_bytes', String(options.target_size_bytes))
if (options.output_format) form.append('output_format', options.output_format)
if (options.max_width) form.append('max_width', String(options.max_width))
if (options.max_height) form.append('max_height', String(options.max_height))