feat: 调整压缩&转换逻辑

This commit is contained in:
chaos-zhu committed 2026-02-26 21:57:46 +08:00
1 parent 078b470168
commit 0d8c39591a
6 files changed
+246 -164

No files matched your search

+206 -142
View File
@@ -86,7 +86,7 @@
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">开启压缩</label>
<p class="text-xs text-gray-500 dark:text-gray-400">压缩上传的图片(默认不转换格式)</p>
<p class="text-xs text-gray-500 dark:text-gray-400">压缩上传的图片,不影响格式转换</p>
</div>
<button
type="button"
@@ -101,63 +101,81 @@
</button>
</div>
<!-- 压缩选项 -->
<div v-if="publicConfig.enableCompression" class="ml-4 space-y-3">
<!-- 压缩质量 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
压缩质量 ({{ publicConfig.compressionQuality }}%)
</label>
<input
type="range"
v-model.number="publicConfig.compressionQuality"
min="10"
max="100"
step="5"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
<!-- 压缩质量(开启压缩后显示) -->
<div v-if="publicConfig.enableCompression" class="ml-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
压缩质量 ({{ publicConfig.compressionQuality }}%)
</label>
<input
type="range"
v-model.number="publicConfig.compressionQuality"
min="10"
max="100"
step="5"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
<!-- 转换格式选项组(互斥) -->
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">转换格式(二选一,可都不选)</label>
<!-- 转为 WebP -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 WebP</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为 WebP 格式(不可与转为 PNG 同时开启)</p>
</div>
<button
type="button"
@click="togglePublicConvertFormat('webp')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="publicConfig.convertToWebp ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="publicConfig.convertToWebp ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 PNG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 PNG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为 PNG 格式(不可与转为 WebP 同时开启)</p>
</div>
<button
type="button"
@click="togglePublicConvertFormat('png')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="publicConfig.convertToPng ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="publicConfig.convertToPng ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 格式转换(独立于压缩) -->
<div class="space-y-2">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">格式转换</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为指定格式(三选一,可都不选),独立于压缩功能</p>
</div>
<!-- 转为 WebP -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 WebP</label>
<p class="text-xs text-gray-500 dark:text-gray-400">体积更小,现代浏览器兼容</p>
</div>
<button
type="button"
@click="togglePublicConvertFormat('webp')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="publicConfig.convertToWebp ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="publicConfig.convertToWebp ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 JPG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 JPG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">通用格式,兼容性最佳</p>
</div>
<button
type="button"
@click="togglePublicConvertFormat('jpg')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="publicConfig.convertToJpg ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="publicConfig.convertToJpg ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 PNG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 PNG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">无损格式,支持透明通道</p>
</div>
<button
type="button"
@click="togglePublicConvertFormat('png')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="publicConfig.convertToPng ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="publicConfig.convertToPng ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
</div>
@@ -503,86 +521,6 @@
/>
</div>
<!-- 开启压缩 -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">开启压缩</label>
<p class="text-xs text-gray-500 dark:text-gray-400">压缩上传的图片(默认不转换格式)</p>
</div>
<button
type="button"
@click="privateConfig.enableCompression = !privateConfig.enableCompression"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.enableCompression ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.enableCompression ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 压缩选项 -->
<div v-if="privateConfig.enableCompression" class="ml-4 space-y-3">
<!-- 压缩质量 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
压缩质量 ({{ privateConfig.compressionQuality }}%)
</label>
<input
type="range"
v-model.number="privateConfig.compressionQuality"
min="10"
max="100"
step="5"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
<!-- 转换格式选项组(互斥) -->
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">转换格式(二选一,可都不选)</label>
<!-- 转为 WebP -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 WebP</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为 WebP 格式(不可与转为 PNG 同时开启)</p>
</div>
<button
type="button"
@click="togglePrivateConvertFormat('webp')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.convertToWebp ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.convertToWebp ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 PNG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 PNG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为 PNG 格式(不可与转为 WebP 同时开启)</p>
</div>
<button
type="button"
@click="togglePrivateConvertFormat('png')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.convertToPng ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.convertToPng ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
</div>
</div>
</div>
<!-- 首页展示 -->
<div class="flex items-center justify-between">
<div>
@@ -602,6 +540,104 @@
</button>
</div>
<!-- 开启压缩 -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">开启压缩</label>
<p class="text-xs text-gray-500 dark:text-gray-400">压缩上传的图片,不影响格式转换</p>
</div>
<button
type="button"
@click="privateConfig.enableCompression = !privateConfig.enableCompression"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.enableCompression ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.enableCompression ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 压缩质量(开启压缩后显示) -->
<div v-if="privateConfig.enableCompression" class="ml-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
压缩质量 ({{ privateConfig.compressionQuality }}%)
</label>
<input
type="range"
v-model.number="privateConfig.compressionQuality"
min="10"
max="100"
step="5"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
<!-- 格式转换(独立于压缩) -->
<div class="space-y-2">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">格式转换</label>
<p class="text-xs text-gray-500 dark:text-gray-400">将图片转换为指定格式(三选一,可都不选),独立于压缩功能</p>
</div>
<!-- 转为 WebP -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 WebP</label>
<p class="text-xs text-gray-500 dark:text-gray-400">体积更小,现代浏览器兼容</p>
</div>
<button
type="button"
@click="togglePrivateConvertFormat('webp')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.convertToWebp ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.convertToWebp ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 JPG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 JPG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">通用格式,兼容性最佳</p>
</div>
<button
type="button"
@click="togglePrivateConvertFormat('jpg')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.convertToJpg ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.convertToJpg ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- 转为 PNG -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">转为 PNG</label>
<p class="text-xs text-gray-500 dark:text-gray-400">无损格式,支持透明通道</p>
</div>
<button
type="button"
@click="togglePrivateConvertFormat('png')"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="privateConfig.convertToPng ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="privateConfig.convertToPng ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
</div>
<div class="pt-4">
<button type="submit" class="btn-primary" :disabled="saving">
{{ saving ? '保存中...' : '保存配置' }}
@@ -1001,6 +1037,7 @@ const publicConfig = reactive({
compressionQuality: 80,
convertToWebp: false,
convertToPng: false,
convertToJpg: false,
rateLimit: 10,
allowConcurrent: false,
contentSafety: { ...defaultContentSafetyConfig }
@@ -1013,6 +1050,7 @@ const privateConfig = reactive({
compressionQuality: 80,
convertToWebp: false,
convertToPng: false,
convertToJpg: false,
showOnHomepage: false
})
@@ -1122,25 +1160,49 @@ async function toggleContentSafety() {
}
}
// 互斥切换公共配置格式转换
// 互斥切换公共配置格式转换(三选一)
function togglePublicConvertFormat(format) {
if (format === 'webp') {
publicConfig.convertToWebp = !publicConfig.convertToWebp
if (publicConfig.convertToWebp) publicConfig.convertToPng = false
if (publicConfig.convertToWebp) {
publicConfig.convertToPng = false
publicConfig.convertToJpg = false
}
} else if (format === 'jpg') {
publicConfig.convertToJpg = !publicConfig.convertToJpg
if (publicConfig.convertToJpg) {
publicConfig.convertToWebp = false
publicConfig.convertToPng = false
}
} else if (format === 'png') {
publicConfig.convertToPng = !publicConfig.convertToPng
if (publicConfig.convertToPng) publicConfig.convertToWebp = false
if (publicConfig.convertToPng) {
publicConfig.convertToWebp = false
publicConfig.convertToJpg = false
}
}
}
// 互斥切换私有配置格式转换
// 互斥切换私有配置格式转换(三选一)
function togglePrivateConvertFormat(format) {
if (format === 'webp') {
privateConfig.convertToWebp = !privateConfig.convertToWebp
if (privateConfig.convertToWebp) privateConfig.convertToPng = false
if (privateConfig.convertToWebp) {
privateConfig.convertToPng = false
privateConfig.convertToJpg = false
}
} else if (format === 'jpg') {
privateConfig.convertToJpg = !privateConfig.convertToJpg
if (privateConfig.convertToJpg) {
privateConfig.convertToWebp = false
privateConfig.convertToPng = false
}
} else if (format === 'png') {
privateConfig.convertToPng = !privateConfig.convertToPng
if (privateConfig.convertToPng) privateConfig.convertToWebp = false
if (privateConfig.convertToPng) {
privateConfig.convertToWebp = false
privateConfig.convertToJpg = false
}
}
}
@@ -1156,6 +1218,7 @@ async function savePublicConfig() {
compressionQuality: publicConfig.compressionQuality,
convertToWebp: publicConfig.convertToWebp,
convertToPng: publicConfig.convertToPng,
convertToJpg: publicConfig.convertToJpg,
rateLimit: publicConfig.rateLimit,
allowConcurrent: publicConfig.allowConcurrent,
contentSafety: publicConfig.contentSafety
@@ -1183,6 +1246,7 @@ async function savePrivateConfig() {
compressionQuality: privateConfig.compressionQuality,
convertToWebp: privateConfig.convertToWebp,
convertToPng: privateConfig.convertToPng,
convertToJpg: privateConfig.convertToJpg,
showOnHomepage: privateConfig.showOnHomepage
})
+1
View File
@@ -33,6 +33,7 @@ export default defineEventHandler(async (event) => {
compressionQuality: 80,
convertToWebp: false,
convertToPng: false,
convertToJpg: false,
showOnHomepage: false
}
}
+5 -2
View File
@@ -30,12 +30,14 @@ export default defineEventHandler(async (event) => {
compressionQuality,
convertToWebp,
convertToPng,
convertToJpg,
showOnHomepage
} = body
// 构建更新对象(convertToWebp 和 convertToPng 互斥)
// 构建更新对象(格式转换三选一:webp > jpg > png 优先级)
const finalConvertToWebp = convertToWebp || false
const finalConvertToPng = finalConvertToWebp ? false : (convertToPng || false)
const finalConvertToJpg = finalConvertToWebp ? false : (convertToJpg || false)
const finalConvertToPng = (finalConvertToWebp || finalConvertToJpg) ? false : (convertToPng || false)
const configValue = {
maxFileSize: maxFileSize || 100 * 1024 * 1024,
@@ -43,6 +45,7 @@ export default defineEventHandler(async (event) => {
compressionQuality: compressionQuality || 80,
convertToWebp: finalConvertToWebp,
convertToPng: finalConvertToPng,
convertToJpg: finalConvertToJpg,
showOnHomepage: showOnHomepage === true
}
+1
View File
@@ -24,6 +24,7 @@ export default defineEventHandler(async (event) => {
compressionQuality: 80,
convertToWebp: false,
convertToPng: false,
convertToJpg: false,
rateLimit: 10,
allowConcurrent: false,
contentSafety: getDefaultContentSafetyConfig()
+5 -2
View File
@@ -33,6 +33,7 @@ export default defineEventHandler(async (event) => {
compressionQuality,
convertToWebp,
convertToPng,
convertToJpg,
rateLimit,
allowConcurrent,
contentSafety
@@ -63,9 +64,10 @@ export default defineEventHandler(async (event) => {
}
}
// 构建更新对象(convertToWebp 和 convertToPng 互斥)
// 构建更新对象(格式转换三选一:webp > jpg > png 优先级)
const finalConvertToWebp = convertToWebp || false
const finalConvertToPng = finalConvertToWebp ? false : (convertToPng || false)
const finalConvertToJpg = finalConvertToWebp ? false : (convertToJpg || false)
const finalConvertToPng = (finalConvertToWebp || finalConvertToJpg) ? false : (convertToPng || false)
const configValue = {
enabled: enabled !== undefined ? enabled : true,
@@ -75,6 +77,7 @@ export default defineEventHandler(async (event) => {
compressionQuality: compressionQuality || 80,
convertToWebp: finalConvertToWebp,
convertToPng: finalConvertToPng,
convertToJpg: finalConvertToJpg,
rateLimit: rateLimit || 10,
allowConcurrent: allowConcurrent || false,
contentSafety: contentSafetyConfig
+28 -18
View File
@@ -7,7 +7,9 @@ import db from './db.js'
/**
* 根据配置处理图片格式转换/压缩
* convertToWebp 与 convertToPng 互斥,WebP 优先级更高
* 压缩与格式转换相互独立:
* - enableCompression:仅控制是否压缩(按质量压缩),不影响格式转换
* - convertToWebp / convertToPng / convertToJpg:格式转换独立开关(三选一,webp > jpg > png 优先级)
* @param {Buffer} buffer - 原始图片数据
* @param {string} fileExt - 原始文件扩展名
* @param {Object} config - 配置对象
@@ -15,6 +17,7 @@ import db from './db.js'
* @param {number} config.compressionQuality - 压缩质量 (0-100)
* @param {boolean} config.convertToWebp - 是否转换为 WebP
* @param {boolean} config.convertToPng - 是否转换为 PNG
* @param {boolean} config.convertToJpg - 是否转换为 JPG
* @param {number} [minSizeKb=0] - 触发压缩的最小文件大小(KB),0 表示不限
* @returns {Promise<{processedBuffer: Buffer, finalFormat: string, isWebp: boolean}>}
*/
@@ -23,30 +26,37 @@ export async function processImageWithConfig(buffer, fileExt, config, minSizeKb
let finalFormat = fileExt
let isWebp = false
const isGif = fileExt === 'gif'
const fileSizeInKb = buffer.length / 1024
const shouldProcess = config.enableCompression
&& fileExt !== 'gif'
&& (minSizeKb === 0 || fileSizeInKb > minSizeKb)
if (shouldProcess) {
const processOptions = {
quality: config.compressionQuality || 80
}
// 确定目标格式(格式转换独立于压缩)
let targetFormat = fileExt
if (!isGif) {
if (config.convertToWebp) {
// 转为 WebP(优先级高于 PNG)
processOptions.format = 'webp'
finalFormat = 'webp'
targetFormat = 'webp'
isWebp = true
} else if (config.convertToJpg) {
targetFormat = 'jpg'
} else if (config.convertToPng) {
// 转为 PNG
processOptions.format = 'png'
finalFormat = 'png'
} else {
// 保持原格式压缩
processOptions.format = fileExt
targetFormat = 'png'
}
}
finalFormat = targetFormat
// 判断是否需要格式转换
const needConvert = !isGif && targetFormat !== fileExt
// 判断是否需要压缩(低于 100KB 的图片不执行压缩,格式转换不受此限制)
const compressMinSizeKb = minSizeKb > 0 ? minSizeKb : 100
const needCompress = config.enableCompression
&& !isGif
&& fileSizeInKb > compressMinSizeKb
if (needConvert || needCompress) {
const processOptions = {
format: targetFormat === 'jpg' ? 'jpeg' : targetFormat,
quality: needCompress ? (config.compressionQuality || 80) : 100
}
processedBuffer = await processImage(buffer, processOptions)
}